diff --git a/.editorconfig b/.editorconfig
index 8cdb92d11c..67f98f94eb 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -135,7 +135,7 @@ csharp_preferred_modifier_order = public,private,protected,internal,new,abstract
csharp_style_expression_bodied_accessors = true:warning
csharp_style_expression_bodied_constructors = false:none
csharp_style_expression_bodied_indexers = true:warning
-csharp_style_expression_bodied_methods = true:silent
+csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_operators = true:warning
csharp_style_expression_bodied_properties = true:warning
csharp_style_expression_bodied_local_functions = true:silent
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/.gitignore b/.gitignore
index e6b5db5904..732b171f69 100644
--- a/.gitignore
+++ b/.gitignore
@@ -331,3 +331,6 @@ fastlane/report.xml
# inspectcode
inspectcodereport.xml
inspectcode
+
+# BenchmarkDotNet
+/BenchmarkDotNet.Artifacts
diff --git a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml
index 7515e76054..4bb9f4d2a0 100644
--- a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml
+++ b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml
new file mode 100644
index 0000000000..1815c271b4
--- /dev/null
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml
index a4154623b6..512ac4393a 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -15,7 +15,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml
index 080dc04001..dec1ef717f 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -15,7 +15,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml
index 3de6a7e609..d9370d5440 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -15,7 +15,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml
index da14c2a29e..def4940bb1 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -15,7 +15,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml
index 45d1ce25e9..1ffa73c257 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -14,7 +14,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml
index ba80f7c100..e64da796b7 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -15,7 +15,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml
index 911c3ed9b7..22105e1de2 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -14,7 +14,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml
new file mode 100644
index 0000000000..31f1fda09d
--- /dev/null
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml
index ec3c81f4cd..cc243f6901 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -14,7 +14,7 @@
-
+
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index e7b691909e..4e8af405a2 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -1,6 +1,7 @@
{
"version": "0.2.0",
- "configurations": [{
+ "configurations": [
+ {
"name": "osu! (Debug)",
"type": "coreclr",
"request": "launch",
@@ -10,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"
},
{
@@ -27,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"
},
{
@@ -44,13 +35,9 @@
],
"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"
- }, {
+ },
+ {
"name": "osu! (Tests, Release)",
"type": "coreclr",
"request": "launch",
@@ -60,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"
},
{
@@ -78,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"
},
{
@@ -96,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"
},
{
@@ -114,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"
},
{
@@ -132,11 +99,19 @@
],
"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"
+ },
+ {
+ "name": "Benchmark",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "${workspaceRoot}/osu.Game.Benchmarks/bin/Release/netcoreapp3.1/osu.Game.Benchmarks.dll",
+ "args": [
+ "--filter",
+ "*"
+ ],
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Build benchmarks",
"console": "internalConsole"
},
{
@@ -154,4 +129,4 @@
"externalConsole": false
}
]
-}
\ No newline at end of file
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index c087800d2a..e638dec767 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -2,7 +2,8 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
- "tasks": [{
+ "tasks": [
+ {
"label": "Build osu! (Debug)",
"type": "shell",
"command": "dotnet",
@@ -78,7 +79,8 @@
],
"group": "build",
"problemMatcher": "$msCompile"
- }, {
+ },
+ {
"label": "Build tournament tests (Release)",
"type": "shell",
"command": "dotnet",
@@ -94,6 +96,22 @@
"group": "build",
"problemMatcher": "$msCompile"
},
+ {
+ "label": "Build benchmarks",
+ "type": "shell",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "--no-restore",
+ "osu.Game.Benchmarks",
+ "/p:Configuration=Release",
+ "/p:GenerateFullPaths=true",
+ "/m",
+ "/verbosity:m"
+ ],
+ "group": "build",
+ "problemMatcher": "$msCompile"
+ },
{
"label": "Restore (netcoreapp3.1)",
"type": "shell",
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000000..6c327f01b3
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,137 @@
+# Contributing Guidelines
+
+Thank you for showing interest in the development of osu!lazer! We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience.
+
+These are not "official rules" *per se*, but following them will help everyone deal with things in the most efficient manner.
+
+## Table of contents
+
+1. [I would like to submit an issue!](#i-would-like-to-submit-an-issue)
+2. [I would like to submit a pull request!](#i-would-like-to-submit-a-pull-request)
+
+## I would like to submit an issue!
+
+Issues, bug reports and feature suggestions are welcomed, though please keep in mind that at any point in time, hundreds of issues are open, which vary in severity and the amount of time needed to address them. As such it's not uncommon for issues to remain unresolved for a long time or even closed outright if they are deemed not important enough to fix in the foreseeable future. Issues that are required to "go live" or otherwise achieve parity with stable are prioritised the most.
+
+* **Before submitting an issue, try searching existing issues first.**
+
+ For housekeeping purposes, we close issues that overlap with or duplicate other pre-existing issues - you can help us not to have to do that by searching existing issues yourself first. The issue search box, as well as the issue tag system, are tools you can use to check if an issue has been reported before.
+
+* **When submitting a bug report, please try to include as much detail as possible.**
+
+ Bugs are not equal - some of them will be reproducible every time on pretty much all hardware, while others will be hard to track down due to being specific to particular hardware or even somewhat random in nature. As such, providing as much detail as possible when reporting a bug is hugely appreciated. A good starting set of information consists of:
+
+ * the in-game logs, which are located at:
+ * `%AppData%/osu/logs` (on Windows),
+ * `~/.local/share/osu/logs` (on Linux and macOS),
+ * `Android/Data/sh.ppy.osulazer/logs` (on Android),
+ * on iOS they can be obtained by connecting your device to your desktop and [copying the `logs` directory from the app's own document storage using iTunes](https://support.apple.com/en-us/HT201301#copy-to-computer),
+ * your system specifications (including the operating system and platform you are playing on),
+ * a reproduction scenario (list of steps you have performed leading up to the occurrence of the bug),
+ * a video or picture of the bug, if at all possible.
+
+* **Provide more information when asked to do so.**
+
+ Sometimes when a bug is more elusive or complicated, none of the information listed above will pinpoint a concrete cause of the problem. In this case we will most likely ask you for additional info, such as a Windows Event Log dump or a copy of your local lazer database (`client.db`). Providing that information is beneficial to both parties - we can track down the problem better, and hopefully fix it for you at some point once we know where it is!
+
+* **When submitting a feature proposal, please describe it in the most understandable way you can.**
+
+ Communicating your idea for a feature can often be hard, and we would like to avoid any misunderstandings. As such, please try to explain your idea in a short, but understandable manner - it's best to avoid jargon or terms and references that could be considered obscure. A mock-up picture (doesn't have to be good!) of the feature can also go a long way in explaining.
+
+* **Refrain from posting "+1" comments.**
+
+ If an issue has already been created, saying that you also experience it without providing any additional details doesn't really help us in any way. To express support for a proposal or indicate that you are also affected by a particular bug, you can use comment reactions instead.
+
+* **Refrain from asking if an issue has been resolved yet.**
+
+ As mentioned above, the issue tracker has hundreds of issues open at any given time. Currently the game is being worked on by two members of the core team, and a handful of outside contributors who offer their free time to help out. As such, it can happen that an issue gets placed on the backburner due to being less important; generally posting a comment demanding its resolution some months or years after it is reported is not very likely to increase its priority.
+
+* **Avoid long discussions about non-development topics.**
+
+ GitHub is mostly a developer space, and as such isn't really fit for lengthened discussions about gameplay mechanics (which might not even be in any way confirmed for the final release) and similar non-technical matters. Such matters are probably best addressed at the osu! forums.
+
+## I would like to submit a pull request!
+
+We also welcome pull requests from unaffiliated contributors. The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues that you can work on; we also mark issues that we think would be good for newcomers with the [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label.
+
+However, do keep in mind that the core team is committed to bringing osu!lazer up to par with stable first and foremost, so depending on what your contribution concerns, it might not be merged and released right away. Our approach to managing issues and their priorities is described [in the wiki](https://github.com/ppy/osu/wiki/Project-management).
+
+Here are some key things to note before jumping in:
+
+* **Make sure you are comfortable with C\# and your development environment.**
+
+ While we are accepting of all kinds of contributions, we also have a certain quality standard we'd like to uphold and limited time to review your code. Therefore, we would like to avoid providing entry-level advice, and as such if you're not very familiar with C\# as a programming language, we'd recommend that you start off with a few personal projects to get acquainted with the language's syntax, toolchain and principles of object-oriented programming first.
+
+ In addition, please take the time to take a look at and get acquainted with the [development and testing](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) procedure we have set up.
+
+* **Make sure you are familiar with git and the pull request workflow.**
+
+ [git](https://git-scm.com/) is a distributed version control system that might not be very intuitive at the beginning if you're not familiar with version control. In particular, projects using git have a particular workflow for submitting code changes, which is called the pull request workflow.
+
+ To make things run more smoothly, we recommend that you look up some online resources to familiarise yourself with the git vocabulary and commands, and practice working with forks and submitting pull requests at your own pace. A high-level overview of the process can be found in [this article by GitHub](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests).
+
+* **Double-check designs before starting work on new functionality.**
+
+ When implementing new features, 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.
+
+* **Make sure to submit pull requests off of a topic branch.**
+
+ As described in the article linked in the previous point, topic branches help you parallelise your work and separate it from the main `master` branch, and additionally are easier for maintainers to work with. Working with multiple `master` branches across many remotes is difficult to keep track of, and it's easy to make a mistake and push to the wrong `master` branch by accident.
+
+* **Refrain from making changes through the GitHub web interface.**
+
+ Even though GitHub provides an option to edit code or replace files in the repository using the web interface, we strongly discourage using it in most scenarios. Editing files this way is inefficient and likely to introduce whitespace or file encoding changes that make it more difficult to review the code.
+
+ Code written through the web interface will also very likely be questioned outright by the reviewers, as it is likely that it has not been properly tested or that it will fail continuous integration checks. We strongly encourage using an IDE like [Visual Studio](https://visualstudio.microsoft.com/), [Visual Studio Code](https://code.visualstudio.com/) or [JetBrains Rider](https://www.jetbrains.com/rider/) instead.
+
+* **Add tests for your code whenever possible.**
+
+ Automated tests are an essential part of a quality and reliable codebase. They help to make the code more maintainable by ensuring it is safe to reorganise (or refactor) the code in various ways, and also prevent regressions - bugs that resurface after having been fixed at some point in the past. If it is viable, please put in the time to add tests, so that the changes you make can last for a (hopefully) very long time.
+
+* **Run tests before opening a pull request.**
+
+ Tying into the previous point, sometimes changes in one part of the codebase can result in unpredictable changes in behaviour in other pieces of the code. This is why it is best to always try to run tests before opening a PR.
+
+ Continuous integration will always run the tests for you (and us), too, but it is best not to rely on it, as there might be many builds queued at any time. Running tests on your own will help you be more certain that at the point of clicking the "Create pull request" button, your changes are as ready as can be.
+
+* **Run code style analysis before opening a pull request.**
+
+ As part of continuous integration, we also run code style analysis, which is supposed to make sure that your code is formatted the same way as all the pre-existing code in the repository. The reason we enforce a particular code style everywhere is to make sure the codebase is consistent in that regard - having one whitespace convention in one place and another one elsewhere causes disorganisation.
+
+* **Make sure that the pull request is complete before opening it.**
+
+ Whether it's fixing a bug or implementing new functionality, it's best that you make sure that the change you want to submit as a pull request is as complete as it can be before clicking the *Create pull request* button. Having to track if a pull request is ready for review or not places additional burden on reviewers.
+
+ Draft pull requests are an option, but use them sparingly and within reason. They are best suited to discuss code changes that cannot be easily described in natural language or have a potential large impact on the future direction of the project. When in doubt, don't open drafts unless a maintainer asks you to do so.
+
+* **Only push code when it's ready.**
+
+ As an extension of the above, when making changes to an already-open PR, please try to only push changes you are reasonably certain of. Pushing after every commit causes the continuous integration build queue to grow in size, slowing down work and taking up time that could be spent verifying other changes.
+
+* **Make sure to keep the *Allow edits from maintainers* check box checked.**
+
+ To speed up the merging process, collaborators and team members will sometimes want to push changes to your branch themselves, to make minor code style adjustments or to otherwise refactor the code without having to describe how they'd like the code to look like in painstaking detail. Having the *Allow edits from maintainers* check box checked lets them do that; without it they are forced to report issues back to you and wait for you to address them.
+
+* **Refrain from continually merging the master branch back to the PR.**
+
+ Unless there are merge conflicts that need resolution, there is no need to keep merging `master` back to a branch over and over again. One of the maintainers will merge `master` themselves before merging the PR itself anyway, and continual merge commits can cause CI to get overwhelmed due to queueing up too many builds.
+
+* **Refrain from force-pushing to the PR branch.**
+
+ Force-pushing should be avoided, as it can lead to accidentally overwriting a maintainer's changes or CI building wrong commits. We value all history in the project, so there is no need to squash or amend commits in most cases.
+
+ The cases in which force-pushing is warranted are very rare (such as accidentally leaking sensitive info in one of the files committed, adding unrelated files, or mis-merging a dependent PR).
+
+* **Be patient when waiting for the code to be reviewed and merged.**
+
+ As much as we'd like to review all contributions as fast as possible, our time is limited, as team members have to work on their own tasks in addition to reviewing code. As such, work needs to be prioritised, and it can unfortunately take weeks or months for your PR to be merged, depending on how important it is deemed to be.
+
+* **Don't mistake criticism of code for criticism of your person.**
+
+ As mentioned before, we are highly committed to quality when it comes to the lazer project. This means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state. We try our utmost best to never conflate a person with the code they authored, and to keep the discussion focused on the code at all times. Please consider our comments and requests a learning experience, and don't treat it as a personal attack.
+
+* **Feel free to reach out for help.**
+
+ If you're uncertain about some part of the codebase or some inner workings of the game and framework, please reach out either by leaving a comment in the relevant issue or PR thread, or by posting a message in the [development Discord server](https://discord.gg/ppy). We will try to help you as much as we can.
+
+ When it comes to which form of communication is best, GitHub generally lends better to longer-form discussions, while Discord is better for snappy call-and-response answers. Use your best discretion when deciding, and try to keep a single discussion in one place instead of moving back and forth.
diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index a92191a439..e34626a59e 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -4,3 +4,5 @@ M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(
M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead.
T:System.IComparable;Don't use non-generic IComparable. Use generic version instead.
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
+T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
+T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.
diff --git a/Directory.Build.props b/Directory.Build.props
index 27a0bd0d48..2cd40c8675 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -16,9 +16,9 @@
-
+
-
+
$(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset
@@ -40,7 +40,7 @@
https://github.com/ppy/osu
Automated release.
ppy Pty Ltd
- Copyright (c) 2019 ppy Pty Ltd
+ Copyright (c) 2020 ppy Pty Ltd
osu game
\ No newline at end of file
diff --git a/Gemfile.lock b/Gemfile.lock
index ab594aee74..bf971d2c22 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,10 +1,26 @@
GEM
remote: https://rubygems.org/
specs:
- CFPropertyList (3.0.1)
+ CFPropertyList (3.0.2)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
+ aws-eventstream (1.1.0)
+ aws-partitions (1.329.0)
+ aws-sdk-core (3.99.2)
+ aws-eventstream (~> 1, >= 1.0.2)
+ aws-partitions (~> 1, >= 1.239.0)
+ aws-sigv4 (~> 1.1)
+ jmespath (~> 1.0)
+ aws-sdk-kms (1.34.1)
+ aws-sdk-core (~> 3, >= 3.99.0)
+ aws-sigv4 (~> 1.1)
+ aws-sdk-s3 (1.68.1)
+ aws-sdk-core (~> 3, >= 3.99.0)
+ aws-sdk-kms (~> 1)
+ aws-sigv4 (~> 1.1)
+ aws-sigv4 (1.1.4)
+ aws-eventstream (~> 1.0, >= 1.0.2)
babosa (1.0.3)
claide (1.0.3)
colored (1.2)
@@ -13,36 +29,37 @@ GEM
highline (~> 1.7.2)
declarative (0.0.10)
declarative-option (0.1.0)
- digest-crc (0.4.1)
+ digest-crc (0.5.1)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.5)
emoji_regex (1.0.1)
- excon (0.67.0)
- faraday (0.15.4)
+ excon (0.74.0)
+ faraday (1.0.1)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
faraday (>= 0.7.4)
http-cookie (~> 1.0.0)
- faraday_middleware (0.13.1)
- faraday (>= 0.7.4, < 1.0)
+ faraday_middleware (1.0.0)
+ faraday (~> 1.0)
fastimage (2.1.7)
- fastlane (2.133.0)
+ fastlane (2.149.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
+ aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.2, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
- excon (>= 0.45.0, < 1.0.0)
- faraday (< 0.16.0)
+ excon (>= 0.71.0, < 1.0.0)
+ faraday (>= 0.17, < 2.0)
faraday-cookie_jar (~> 0.0.6)
- faraday_middleware (< 0.16.0)
+ faraday_middleware (>= 0.13.1, < 2.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
- google-api-client (>= 0.21.2, < 0.24.0)
+ google-api-client (>= 0.37.0, < 0.39.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
json (< 3.0.0)
@@ -61,58 +78,61 @@ GEM
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
- xcodeproj (>= 1.8.1, < 2.0.0)
+ xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-clean_testflight_testers (0.3.0)
- fastlane-plugin-souyuz (0.8.1)
- souyuz (>= 0.8.1)
+ fastlane-plugin-souyuz (0.9.1)
+ souyuz (= 0.9.1)
fastlane-plugin-xamarin (0.6.3)
gh_inspector (1.1.3)
- google-api-client (0.23.9)
+ google-api-client (0.38.0)
addressable (~> 2.5, >= 2.5.1)
- googleauth (>= 0.5, < 0.7.0)
+ googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
- mime-types (~> 3.0)
+ mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
- signet (~> 0.9)
- google-cloud-core (1.3.1)
+ signet (~> 0.12)
+ google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
- google-cloud-env (1.2.1)
- faraday (~> 0.11)
- google-cloud-storage (1.16.0)
+ google-cloud-errors (~> 1.0)
+ google-cloud-env (1.3.2)
+ faraday (>= 0.17.3, < 2.0)
+ google-cloud-errors (1.0.1)
+ google-cloud-storage (1.26.2)
+ addressable (~> 2.5)
digest-crc (~> 0.4)
- google-api-client (~> 0.23)
+ google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
- googleauth (>= 0.6.2, < 0.10.0)
- googleauth (0.6.7)
- faraday (~> 0.12)
+ googleauth (~> 0.9)
+ mini_mime (~> 1.0)
+ googleauth (0.12.0)
+ faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
- signet (~> 0.7)
+ signet (~> 0.14)
highline (1.7.10)
http-cookie (1.0.3)
domain_name (~> 0.5)
httpclient (2.8.3)
- json (2.2.0)
+ jmespath (1.4.0)
+ json (2.3.0)
jwt (2.1.0)
- memoist (0.16.0)
- mime-types (3.3)
- mime-types-data (~> 3.2015)
- mime-types-data (3.2019.1009)
- mini_magick (4.9.5)
+ memoist (0.16.2)
+ mini_magick (4.10.1)
+ mini_mime (1.0.2)
mini_portile2 (2.4.0)
- multi_json (1.13.1)
+ multi_json (1.14.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
nanaimo (0.2.6)
naturally (2.2.0)
- nokogiri (1.10.4)
+ nokogiri (1.10.7)
mini_portile2 (~> 2.4.0)
- os (1.0.1)
+ os (1.1.0)
plist (3.5.0)
public_suffix (2.0.5)
representable (3.0.4)
@@ -123,33 +143,33 @@ GEM
rouge (2.0.7)
rubyzip (1.3.0)
security (0.1.3)
- signet (0.12.0)
+ signet (0.14.0)
addressable (~> 2.3)
- faraday (~> 0.9)
+ faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
- simctl (1.6.6)
+ simctl (1.6.8)
CFPropertyList
naturally
slack-notifier (2.3.2)
- souyuz (0.8.1)
- fastlane (>= 2.29.0)
+ souyuz (0.9.1)
+ fastlane (>= 1.103.0)
highline (~> 1.7)
nokogiri (~> 1.7)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
- tty-cursor (0.7.0)
- tty-screen (0.7.0)
- tty-spinner (0.9.1)
+ tty-cursor (0.7.1)
+ tty-screen (0.8.0)
+ tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
- unf_ext (0.0.7.6)
- unicode-display_width (1.6.0)
+ unf_ext (0.0.7.7)
+ unicode-display_width (1.7.0)
word_wrap (1.0.0)
- xcodeproj (1.12.0)
+ xcodeproj (1.16.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
diff --git a/LICENCE b/LICENCE
index 21c6a7090f..2435c23545 100644
--- a/LICENCE
+++ b/LICENCE
@@ -1,4 +1,4 @@
-Copyright (c) 2019 ppy Pty Ltd .
+Copyright (c) 2020 ppy Pty Ltd .
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 67027bb9f3..dc3ee63844 100644
--- a/README.md
+++ b/README.md
@@ -13,35 +13,38 @@ 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.
+**IMPORTANT:** Gameplay mechanics (and other features which you may have come to know and love) are in a constant state of flux. Game balance and final quality-of-life passses come at the end of development, preceeded by experimentation and changes which may potentially **reduce playability or usability**. This is done in order to allow us to move forward as developers and designers more efficiently. If this offends you, please consider sticking to the stable releases of osu! (found on the website). We are not yet open to heated discussion over game mechanics and will not be using github as a forum for such discussions just yet.
-Detailed changelogs are published on the [official osu! site](https://osu.ppy.sh/home/changelog).
+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:
-## 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:
@@ -90,11 +93,7 @@ JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it
## Contributing
-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).
-
-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**.
+When it comes to contributing to the project, the two main things you can do to help out are reporting issues and submitting pull requests. Based on past experiences, we have prepared a [list of contributing guidelines](CONTRIBUTING.md) that should hopefully ease you into our collaboration process and answer the most frequently-asked questions.
Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured, with any libraries we are using, or with any processes involved with contributing, *please* bring it up. We welcome all feedback so we can make contributing to this project as painless as possible.
diff --git a/build/InspectCode.cake b/build/InspectCode.cake
index 06c56dce87..c8f4f37c94 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=CodeFileSanity&version=0.0.36"
+#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2020.1.3"
#tool "nuget:?package=NVika.MSBuild&version=1.0.1"
var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First();
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 28a83fbbae..4fd0e5e8c7 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -97,8 +97,10 @@ platform :ios do
changelog.gsub!('$BUILD_ID', options[:build])
pilot(
- wait_processing_interval: 1800,
+ wait_processing_interval: 900,
changelog: changelog,
+ groups: ['osu! supporters', 'public'],
+ distribute_external: true,
ipa: './osu.iOS/bin/iPhone/Release/osu.iOS.ipa'
)
end
@@ -111,7 +113,6 @@ platform :ios do
souyuz(
platform: "ios",
- build_target: "osu_iOS",
plist_path: "../osu.iOS/Info.plist"
)
end
diff --git a/global.json b/global.json
index 6858d4044d..233a040d18 100644
--- a/global.json
+++ b/global.json
@@ -5,6 +5,6 @@
"version": "3.1.100"
},
"msbuild-sdks": {
- "Microsoft.Build.Traversal": "2.0.24"
+ "Microsoft.Build.Traversal": "2.0.52"
}
}
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index 0b41d5cda4..71d4e5aacf 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/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index 2e5fa59d20..9839d16030 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -9,7 +9,7 @@ using osu.Framework.Android;
namespace osu.Android
{
- [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = true)]
+ [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
public class OsuGameActivity : AndroidGameActivity
{
protected override Framework.Game CreateGame() => new OsuGameAndroid();
diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs
index a91c010809..7542a2b997 100644
--- a/osu.Android/OsuGameAndroid.cs
+++ b/osu.Android/OsuGameAndroid.cs
@@ -3,6 +3,7 @@
using System;
using Android.App;
+using Android.OS;
using osu.Game;
using osu.Game.Updater;
@@ -18,8 +19,32 @@ namespace osu.Android
try
{
- string versionName = packageInfo.VersionCode.ToString();
- // undo play store version garbling
+ // We store the osu! build number in the "VersionCode" field to better support google play releases.
+ // If we were to use the main build number, it would require a new submission each time (similar to TestFlight).
+ // In order to do this, we should split it up and pad the numbers to still ensure sequential increase over time.
+ //
+ // We also need to be aware that older SDK versions store this as a 32bit int.
+ //
+ // Basic conversion format (as done in Fastfile): 2020.606.0 -> 202006060
+
+ // https://stackoverflow.com/questions/52977079/android-sdk-28-versioncode-in-packageinfo-has-been-deprecated
+ string versionName = string.Empty;
+
+ if (Build.VERSION.SdkInt >= BuildVersionCodes.P)
+ {
+ versionName = packageInfo.LongVersionCode.ToString();
+ // ensure we only read the trailing portion of long (the part we are interested in).
+ versionName = versionName.Substring(versionName.Length - 9);
+ }
+ else
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ // this is required else older SDKs will report missing method exception.
+ versionName = packageInfo.VersionCode.ToString();
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+
+ // undo play store version garbling (as mentioned above).
return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1)));
}
catch
@@ -30,11 +55,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.slnf b/osu.Desktop.slnf
index e6b6446f72..d2c14d321a 100644
--- a/osu.Desktop.slnf
+++ b/osu.Desktop.slnf
@@ -3,6 +3,7 @@
"path": "osu.sln",
"projects": [
"osu.Desktop\\osu.Desktop.csproj",
+ "osu.Game.Benchmarks\\osu.Game.Benchmarks.csproj",
"osu.Game.Rulesets.Catch.Tests\\osu.Game.Rulesets.Catch.Tests.csproj",
"osu.Game.Rulesets.Catch\\osu.Game.Rulesets.Catch.csproj",
"osu.Game.Rulesets.Mania.Tests\\osu.Game.Rulesets.Mania.Tests.csproj",
diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs
index 80bb82c769..08cc0e7f5f 100644
--- a/osu.Desktop/DiscordRichPresence.cs
+++ b/osu.Desktop/DiscordRichPresence.cs
@@ -75,6 +75,9 @@ namespace osu.Desktop
private void updateStatus()
{
+ if (!client.IsInitialized)
+ return;
+
if (status.Value is UserStatusOffline)
{
client.ClearPresence();
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index f70cc24159..cd31df316a 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -6,15 +6,13 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
+using Microsoft.Win32;
using osu.Desktop.Overlays;
using osu.Framework.Platform;
using osu.Game;
-using osuTK.Input;
-using Microsoft.Win32;
using osu.Desktop.Updater;
using osu.Framework;
using osu.Framework.Logging;
-using osu.Framework.Platform.Windows;
using osu.Framework.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Updater;
@@ -37,7 +35,11 @@ namespace osu.Desktop
try
{
if (Host is DesktopGameHost desktopHost)
- return new StableStorage(desktopHost);
+ {
+ string stablePath = getStableInstallPath();
+ if (!string.IsNullOrEmpty(stablePath))
+ return new DesktopStorage(stablePath, desktopHost);
+ }
}
catch (Exception)
{
@@ -47,20 +49,54 @@ namespace osu.Desktop
return null;
}
+ private string getStableInstallPath()
+ {
+ static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
+
+ string stableInstallPath;
+
+ try
+ {
+ using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
+ stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString()?.Split('"')[1].Replace("osu!.exe", "");
+
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+ }
+ catch
+ {
+ }
+
+ stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+
+ stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu");
+ if (checkExists(stableInstallPath))
+ return stableInstallPath;
+
+ 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);
}
@@ -85,66 +121,32 @@ namespace osu.Desktop
{
base.SetHost(host);
- if (host.Window is DesktopGameWindow desktopWindow)
+ switch (host.Window)
{
- desktopWindow.CursorState |= CursorState.Hidden;
+ // Legacy osuTK DesktopGameWindow
+ case DesktopGameWindow desktopGameWindow:
+ desktopGameWindow.CursorState |= CursorState.Hidden;
+ desktopGameWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"));
+ desktopGameWindow.Title = Name;
+ desktopGameWindow.FileDrop += (_, e) => fileDrop(e.FileNames);
+ break;
- desktopWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"));
- desktopWindow.Title = Name;
-
- desktopWindow.FileDrop += fileDrop;
+ // SDL2 DesktopWindow
+ case DesktopWindow desktopWindow:
+ desktopWindow.CursorState.Value |= CursorState.Hidden;
+ desktopWindow.Title = Name;
+ desktopWindow.DragDrop += f => fileDrop(new[] { f });
+ break;
}
}
- private void fileDrop(object sender, FileDropEventArgs e)
+ private void fileDrop(string[] filePaths)
{
- var filePaths = e.FileNames;
-
var firstExtension = Path.GetExtension(filePaths.First());
if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning);
}
-
- ///
- /// A method of accessing an osu-stable install in a controlled fashion.
- ///
- private class StableStorage : WindowsStorage
- {
- protected override string LocateBasePath()
- {
- static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
-
- string stableInstallPath;
-
- try
- {
- using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
- stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
-
- if (checkExists(stableInstallPath))
- return stableInstallPath;
- }
- catch
- {
- }
-
- stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
- if (checkExists(stableInstallPath))
- return stableInstallPath;
-
- stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu");
- if (checkExists(stableInstallPath))
- return stableInstallPath;
-
- return null;
- }
-
- public StableStorage(DesktopGameHost host)
- : base(string.Empty, host)
- {
- }
- }
}
}
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index 141b2cdbbc..285a813d97 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -22,8 +22,9 @@ namespace osu.Desktop
{
// Back up the cwd before DesktopGameHost changes it
var cwd = Environment.CurrentDirectory;
+ bool useSdl = args.Contains("--sdl");
- using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
+ using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true, useSdl: useSdl))
{
host.ExceptionThrown += handleException;
@@ -32,13 +33,11 @@ namespace osu.Desktop
if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args
{
var importer = new ArchiveImportIPCChannel(host);
- // Restore the cwd so relative paths given at the command line work correctly
- Directory.SetCurrentDirectory(cwd);
foreach (var file in args)
{
Console.WriteLine(@"Importing {0}", file);
- if (!importer.ImportAsync(Path.GetFullPath(file)).Wait(3000))
+ if (!importer.ImportAsync(Path.GetFullPath(file, cwd)).Wait(3000))
throw new TimeoutException(@"IPC took too long to send");
}
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index 60b47a8b3a..05c8e835ac 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -30,29 +30,27 @@ namespace osu.Desktop.Updater
private static readonly Logger logger = Logger.GetLogger("updater");
[BackgroundDependencyLoader]
- private void load(NotificationOverlay notification, OsuGameBase game)
+ private void load(NotificationOverlay notification)
{
notificationOverlay = notification;
- if (game.IsDeployedBuild)
- {
- Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
- Schedule(() => Task.Run(() => checkForUpdateAsync()));
- }
+ Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
}
- private async void checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
+ protected override async Task PerformUpdateCheck() => await checkForUpdateAsync();
+
+ private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
{
- //should we schedule a retry on completion of this check?
+ // should we schedule a retry on completion of this check?
bool scheduleRecheck = true;
try
{
- if (updateManager == null) updateManager = await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true);
+ updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true);
var info = await updateManager.CheckForUpdate(!useDeltaPatching);
if (info.ReleasesToApply.Count == 0)
- //no updates available. bail and retry later.
+ // no updates available. bail and retry later.
return;
if (notification == null)
@@ -81,9 +79,9 @@ namespace osu.Desktop.Updater
{
logger.Add(@"delta patching failed; will attempt full download!");
- //could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
- //try again without deltas.
- checkForUpdateAsync(false, notification);
+ // could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
+ // try again without deltas.
+ await checkForUpdateAsync(false, notification);
scheduleRecheck = false;
}
else
@@ -101,8 +99,8 @@ namespace osu.Desktop.Updater
{
if (scheduleRecheck)
{
- //check again in 30 minutes.
- Scheduler.AddDelayed(() => checkForUpdateAsync(), 60000 * 30);
+ // check again in 30 minutes.
+ Scheduler.AddDelayed(async () => await checkForUpdateAsync(), 60000 * 30);
}
}
}
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index da47ad8223..7a99c70999 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -29,7 +29,11 @@
-
+
+
+
+
+
diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec
index a26b35fcd5..a919d54f38 100644
--- a/osu.Desktop/osu.nuspec
+++ b/osu.Desktop/osu.nuspec
@@ -12,7 +12,7 @@
click the circles. to the beat.
click the circles.
testing
- Copyright (c) 2019 ppy Pty Ltd
+ Copyright (c) 2020 ppy Pty Ltd
en-AU
diff --git a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs
new file mode 100644
index 0000000000..1d207d04c7
--- /dev/null
+++ b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.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 System.IO;
+using BenchmarkDotNet.Attributes;
+using osu.Framework.IO.Stores;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Formats;
+using osu.Game.IO;
+using osu.Game.IO.Archives;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Benchmarks
+{
+ public class BenchmarkBeatmapParsing : BenchmarkTest
+ {
+ private readonly MemoryStream beatmapStream = new MemoryStream();
+
+ public override void SetUp()
+ {
+ using (var resources = new DllResourceStore(typeof(TestResources).Assembly))
+ using (var archive = resources.GetStream("Resources/Archives/241526 Soleily - Renatus.osz"))
+ using (var reader = new ZipArchiveReader(archive))
+ reader.GetStream("Soleily - Renatus (Gamu) [Insane].osu").CopyTo(beatmapStream);
+ }
+
+ [Benchmark]
+ public Beatmap BenchmarkBundledBeatmap()
+ {
+ beatmapStream.Seek(0, SeekOrigin.Begin);
+ var reader = new LineBufferedReader(beatmapStream); // no disposal
+
+ var decoder = Decoder.GetDecoder(reader);
+ return decoder.Decode(reader);
+ }
+ }
+}
diff --git a/osu.Game.Benchmarks/BenchmarkTest.cs b/osu.Game.Benchmarks/BenchmarkTest.cs
new file mode 100644
index 0000000000..34f5edd084
--- /dev/null
+++ b/osu.Game.Benchmarks/BenchmarkTest.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Running;
+using NUnit.Framework;
+
+namespace osu.Game.Benchmarks
+{
+ [TestFixture]
+ [MemoryDiagnoser]
+ public abstract class BenchmarkTest
+ {
+ [GlobalSetup]
+ [OneTimeSetUp]
+ public virtual void SetUp()
+ {
+ }
+
+ [Test]
+ public void RunBenchmark() => BenchmarkRunner.Run(GetType());
+ }
+}
diff --git a/osu.Game.Benchmarks/Program.cs b/osu.Game.Benchmarks/Program.cs
new file mode 100644
index 0000000000..c55075fea6
--- /dev/null
+++ b/osu.Game.Benchmarks/Program.cs
@@ -0,0 +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 BenchmarkDotNet.Running;
+
+namespace osu.Game.Benchmarks
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ BenchmarkSwitcher
+ .FromAssembly(typeof(Program).Assembly)
+ .Run(args);
+ }
+ }
+}
diff --git a/osu.Game.Benchmarks/Properties/launchSettings.json b/osu.Game.Benchmarks/Properties/launchSettings.json
new file mode 100644
index 0000000000..c1868088f9
--- /dev/null
+++ b/osu.Game.Benchmarks/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "All Benchmarks": {
+ "commandName": "Project",
+ "commandLineArgs": "--filter *"
+ }
+ }
+}
\ No newline at end of file
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
new file mode 100644
index 0000000000..ff26f4afaa
--- /dev/null
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -0,0 +1,20 @@
+
+
+
+ netcoreapp3.1
+ Exe
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
index 493ac7ae39..df54df7b01 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
@@ -5,10 +5,9 @@ using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using NUnit.Framework;
-using osu.Framework.MathUtils;
+using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
@@ -83,7 +82,7 @@ namespace osu.Game.Rulesets.Catch.Tests
public float Position
{
- get => HitObject?.X * CatchPlayfield.BASE_WIDTH ?? position;
+ get => HitObject?.X ?? position;
set => position = value;
}
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
index 51fe0b035d..ee416e5a38 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
- [TestCase(4.2058561036909863d, "diffcalc-test")]
+ [TestCase(4.050601681491468d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs
new file mode 100644
index 0000000000..b570f090ca
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.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.IO.Stores;
+using osu.Game.Rulesets.Catch.Skinning;
+using osu.Game.Skinning;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ [TestFixture]
+ public class CatchSkinColourDecodingTest
+ {
+ [Test]
+ public void TestCatchSkinColourDecoding()
+ {
+ var store = new NamespacedResourceStore(new DllResourceStore(GetType().Assembly), "Resources/special-skin");
+ var rawSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, store);
+ var skinSource = new SkinProvidingContainer(rawSkin);
+ var skin = new CatchLegacySkinTransformer(skinSource);
+
+ Assert.AreEqual(new Color4(232, 185, 35, 255), skin.GetConfig(CatchSkinColour.HyperDash)?.Value);
+ Assert.AreEqual(new Color4(232, 74, 35, 255), skin.GetConfig(CatchSkinColour.HyperDashAfterImage)?.Value);
+ Assert.AreEqual(new Color4(0, 255, 255, 255), skin.GetConfig(CatchSkinColour.HyperDashFruit)?.Value);
+ }
+
+ private class TestLegacySkin : LegacySkin
+ {
+ public TestLegacySkin(SkinInfo skin, IResourceStore storage)
+ // Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null).
+ : base(skin, storage, null, "skin.ini")
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs
new file mode 100644
index 0000000000..378772fea3
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.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.
+
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public abstract class CatchSkinnableTestScene : SkinnableTestScene
+ {
+ protected override Ruleset CreateRulesetForSkinProvider() => new CatchRuleset();
+ }
+}
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..3e06e78dba
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.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.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
+ {
+ protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+
+ public TestSceneCatchModPerfect()
+ : base(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@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail@2x.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@2x.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@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai@2x.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@2x.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/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini
new file mode 100644
index 0000000000..96d50f1451
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini
@@ -0,0 +1,4 @@
+[CatchTheBeat]
+HyperDash: 232,185,35
+HyperDashFruit: 0,255,255
+HyperDashAfterImage: 232,74,35
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
index 74a9c05bf9..d6bba3d55e 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
@@ -7,19 +7,13 @@ 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;
namespace osu.Game.Rulesets.Catch.Tests
{
- public class TestSceneAutoJuiceStream : PlayerTestScene
+ public class TestSceneAutoJuiceStream : TestSceneCatchPlayer
{
- public TestSceneAutoJuiceStream()
- : base(new CatchRuleset())
- {
- }
-
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap
@@ -33,15 +27,15 @@ namespace osu.Game.Rulesets.Catch.Tests
for (int i = 0; i < 100; i++)
{
- float width = (i % 10 + 1) / 20f;
+ float width = (i % 10 + 1) / 20f * CatchPlayfield.WIDTH;
beatmap.HitObjects.Add(new JuiceStream
{
- X = 0.5f - width / 2,
+ X = CatchPlayfield.CENTER_X - width / 2,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
- new Vector2(width * CatchPlayfield.BASE_WIDTH, 0)
+ new Vector2(width, 0)
}),
StartTime = i * 2000,
NewCombo = i % 8 == 0
@@ -51,7 +45,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..e89a95ae37 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
@@ -1,32 +1,19 @@
// 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.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.Objects.Drawable;
-using osu.Game.Rulesets.Catch.UI;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneBananaShower : PlayerTestScene
+ public class TestSceneBananaShower : TestSceneCatchPlayer
{
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(BananaShower),
- typeof(DrawableBananaShower),
-
- typeof(CatchRuleset),
- typeof(DrawableCatchRuleset),
- };
-
- public TestSceneBananaShower()
- : base(new CatchRuleset())
+ [Test]
+ public void TestBananaShower()
{
+ AddUntilStep("player is done", () => !Player.ValidForResume);
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
@@ -40,7 +27,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/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
new file mode 100644
index 0000000000..f15da29993
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.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 System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Catch.Mods;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.UI;
+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 TestSceneCatchModHidden : ModTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ LocalConfig.Set(OsuSetting.IncreaseFirstObjectVisibility, false);
+ }
+
+ [Test]
+ public void TestJuiceStream()
+ {
+ CreateModTest(new ModTestData
+ {
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new JuiceStream
+ {
+ StartTime = 1000,
+ Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(0, -192) }),
+ X = CatchPlayfield.WIDTH / 2
+ }
+ }
+ },
+ Mod = new CatchModHidden(),
+ PassCondition = () => Player.Results.Count > 0
+ && Player.ChildrenOfType().Single().Alpha > 0
+ && Player.ChildrenOfType().Last().Alpha > 0
+ });
+ }
+
+ protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs
index 9836a7811a..31d0831fae 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs
@@ -9,9 +9,6 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestFixture]
public class TestSceneCatchPlayer : PlayerTestScene
{
- public TestSceneCatchPlayer()
- : base(new CatchRuleset())
- {
- }
+ protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs
index 9ce46ad6ba..1ff31697b8 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs
@@ -4,18 +4,13 @@
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Tests.Visual;
+using osu.Game.Rulesets.Catch.UI;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneCatchStacker : PlayerTestScene
+ public class TestSceneCatchStacker : TestSceneCatchPlayer
{
- public TestSceneCatchStacker()
- : base(new CatchRuleset())
- {
- }
-
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap
@@ -28,7 +23,14 @@ namespace osu.Game.Rulesets.Catch.Tests
};
for (int i = 0; i < 512; i++)
- beatmap.HitObjects.Add(new Fruit { X = 0.5f + i / 2048f * (i % 10 - 5), StartTime = i * 100, NewCombo = i % 8 == 0 });
+ {
+ beatmap.HitObjects.Add(new Fruit
+ {
+ X = (0.5f + i / 2048f * (i % 10 - 5)) * CatchPlayfield.WIDTH,
+ StartTime = i * 100,
+ NewCombo = i % 8 == 0
+ });
+ }
return beatmap;
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index 9b529a2e4c..6eeda2c731 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -3,104 +3,24 @@
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;
+using osu.Framework.Graphics.Containers;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneCatcher : OsuTestScene
+ public class TestSceneCatcher : CatchSkinnableTestScene
{
- public override IReadOnlyList RequiredTypes => new[]
- {
- 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(new Container())
{
- 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..b4f123598b 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -1,45 +1,111 @@
// 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.Configuration;
+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.Tests.Visual;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneCatcherArea : OsuTestScene
+ public class TestSceneCatcherArea : CatchSkinnableTestScene
{
private RulesetInfo catchRuleset;
- private TestCatcherArea catcherArea;
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(CatcherArea),
- };
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
+ private Catcher catcher => this.ChildrenOfType().First().MovableCatcher;
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 = catcher.X
+ }), 20);
+ AddRepeatStep("catch fruit last in combo", () => catchFruit(new TestFruit(false)
+ {
+ X = catcher.X,
+ LastInCombo = true,
+ }), 20);
+ AddRepeatStep("catch kiai fruit", () => catchFruit(new TestFruit(true)
+ {
+ X = catcher.X
+ }), 20);
+ AddRepeatStep("miss fruit", () => catchFruit(new Fruit
+ {
+ X = catcher.X + 100,
+ LastInCombo = true,
+ }, true), 20);
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestHitLighting(bool enable)
+ {
+ AddStep("create catcher", () => createCatcher(5));
+
+ AddStep("toggle hit lighting", () => config.Set(OsuSetting.HitLighting, enable));
+ AddStep("catch fruit", () => catchFruit(new TestFruit(false)
+ {
+ X = catcher.X
+ }));
+ AddStep("catch fruit last in combo", () => catchFruit(new TestFruit(false)
+ {
+ X = catcher.X,
+ LastInCombo = true
+ }));
+ AddAssert("check hit explosion", () => catcher.ChildrenOfType().Any() == enable);
+ }
+
+ 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
+ Anchor = Anchor.Centre,
+ Origin = Anchor.TopCentre,
+ CreateDrawableRepresentation = ((DrawableRuleset)catchRuleset.CreateInstance().CreateDrawableRulesetWith(new CatchBeatmap())).CreateDrawableRepresentation
},
- };
+ });
}
[BackgroundDependencyLoader]
@@ -48,6 +114,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 +132,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..d35f828e28 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
@@ -1,16 +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;
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;
@@ -22,20 +21,11 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneDrawableHitObjects : OsuTestScene
{
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(CatcherArea.Catcher),
- typeof(DrawableCatchRuleset),
- typeof(DrawableFruit),
- typeof(DrawableJuiceStream),
- typeof(DrawableBanana)
- };
-
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 +47,7 @@ namespace osu.Game.Rulesets.Catch.Tests
ControlPointInfo = controlPointInfo
});
- Add(new Container
+ Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -66,16 +56,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++)
@@ -135,8 +158,8 @@ namespace osu.Game.Rulesets.Catch.Tests
private float getXCoords(bool hit)
{
- const float x_offset = 0.2f;
- float xCoords = drawableRuleset.Playfield.Width / 2;
+ const float x_offset = 0.2f * CatchPlayfield.WIDTH;
+ float xCoords = CatchPlayfield.CENTER_X;
if (drawableRuleset.Playfield is CatchPlayfield catchPlayfield)
catchPlayfield.CatcherArea.MovableCatcher.X = xCoords - x_offset;
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs
index 8c3dfef39c..62fe5dca2c 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs
@@ -1,9 +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;
-using System.Collections.Generic;
-using System.Linq;
using NUnit.Framework;
using osu.Game.Rulesets.Catch.Mods;
@@ -11,8 +8,6 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneDrawableHitObjectsHidden : TestSceneDrawableHitObjects
{
- public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(CatchModHidden) }).ToList();
-
[SetUp]
public void SetUp() => Schedule(() =>
{
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
index 44517382f7..c07e4fdad3 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
@@ -2,81 +2,98 @@
// 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.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.Tests.Visual;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneFruitObjects : OsuTestScene
+ public class TestSceneFruitObjects : CatchSkinnableTestScene
{
- public override IReadOnlyList RequiredTypes => new[]
+ protected override void LoadComplete()
{
- typeof(CatchHitObject),
- typeof(Fruit),
- typeof(Droplet),
- typeof(DrawableCatchHitObject),
- typeof(DrawableFruit),
- typeof(DrawableDroplet),
- typeof(BananaShower),
- typeof(Pulp),
- };
+ base.LoadComplete();
- public TestSceneFruitObjects()
- {
- Add(new GridContainer
- {
- RelativeSizeAxes = Axes.Both,
- Content = new[]
- {
- new Drawable[]
- {
- createDrawable(0),
- createDrawable(1),
- createDrawable(2),
- },
- new Drawable[]
- {
- createDrawable(3),
- createDrawable(4),
- createDrawable(5),
- },
- }
- });
+ 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..ad24adf352 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
@@ -1,29 +1,40 @@
// 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 NUnit.Framework;
+using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Tests.Visual;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Objects;
+using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneHyperDash : PlayerTestScene
+ public class TestSceneHyperDash : TestSceneCatchPlayer
{
- public TestSceneHyperDash()
- : base(new CatchRuleset())
- {
- }
-
protected override bool Autoplay => true;
[Test]
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 +46,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, NewCombo = true });
+ beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308, 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 * CatchPlayfield.WIDTH;
+ const float right_x = 0.98f * CatchPlayfield.WIDTH;
+
+ 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/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
new file mode 100644
index 0000000000..1e708cce4b
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
@@ -0,0 +1,206 @@
+// 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.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.Skinning;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneHyperDashColouring : OsuTestScene
+ {
+ [Resolved]
+ private SkinManager skins { get; set; }
+
+ [Test]
+ public void TestDefaultCatcherColour()
+ {
+ var skin = new TestSkin();
+
+ checkHyperDashCatcherColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR);
+ }
+
+ [Test]
+ public void TestCustomCatcherColour()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashColour = Color4.Goldenrod
+ };
+
+ checkHyperDashCatcherColour(skin, skin.HyperDashColour);
+ }
+
+ [Test]
+ public void TestCustomEndGlowColour()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashAfterImageColour = Color4.Lime
+ };
+
+ checkHyperDashCatcherColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR, skin.HyperDashAfterImageColour);
+ }
+
+ [Test]
+ public void TestCustomEndGlowColourPriority()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashColour = Color4.Goldenrod,
+ HyperDashAfterImageColour = Color4.Lime
+ };
+
+ checkHyperDashCatcherColour(skin, skin.HyperDashColour, skin.HyperDashAfterImageColour);
+ }
+
+ [Test]
+ public void TestDefaultFruitColour()
+ {
+ var skin = new TestSkin();
+
+ checkHyperDashFruitColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR);
+ }
+
+ [Test]
+ public void TestCustomFruitColour()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashFruitColour = Color4.Cyan
+ };
+
+ checkHyperDashFruitColour(skin, skin.HyperDashFruitColour);
+ }
+
+ [Test]
+ public void TestCustomFruitColourPriority()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashColour = Color4.Goldenrod,
+ HyperDashFruitColour = Color4.Cyan
+ };
+
+ checkHyperDashFruitColour(skin, skin.HyperDashFruitColour);
+ }
+
+ [Test]
+ public void TestFruitColourFallback()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashColour = Color4.Goldenrod
+ };
+
+ checkHyperDashFruitColour(skin, skin.HyperDashColour);
+ }
+
+ private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedEndGlowColour = null)
+ {
+ CatcherArea catcherArea = null;
+ CatcherTrailDisplay trails = null;
+
+ AddStep("create hyper-dashing catcher", () =>
+ {
+ Child = setupSkinHierarchy(catcherArea = new CatcherArea
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(4f),
+ }, skin);
+
+ trails = catcherArea.OfType().Single();
+ catcherArea.MovableCatcher.SetHyperDashState(2);
+ });
+
+ AddUntilStep("catcher colour is correct", () => catcherArea.MovableCatcher.Colour == expectedCatcherColour);
+
+ AddAssert("catcher trails colours are correct", () => trails.HyperDashTrailsColour == expectedCatcherColour);
+ AddAssert("catcher end-glow colours are correct", () => trails.EndGlowSpritesColour == (expectedEndGlowColour ?? expectedCatcherColour));
+
+ AddStep("finish hyper-dashing", () =>
+ {
+ catcherArea.MovableCatcher.SetHyperDashState(1);
+ catcherArea.MovableCatcher.FinishTransforms();
+ });
+
+ AddAssert("catcher colour returned to white", () => catcherArea.MovableCatcher.Colour == Color4.White);
+ }
+
+ private void checkHyperDashFruitColour(ISkin skin, Color4 expectedColour)
+ {
+ DrawableFruit drawableFruit = null;
+
+ AddStep("create hyper-dash fruit", () =>
+ {
+ var fruit = new Fruit { HyperDashTarget = new Banana() };
+ fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ Child = setupSkinHierarchy(drawableFruit = new DrawableFruit(fruit)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(4f),
+ }, skin);
+ });
+
+ AddAssert("hyper-dash colour is correct", () => checkLegacyFruitHyperDashColour(drawableFruit, expectedColour));
+ }
+
+ private Drawable setupSkinHierarchy(Drawable child, ISkin skin)
+ {
+ var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info));
+ var testSkinProvider = new SkinProvidingContainer(skin);
+ var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider));
+
+ return legacySkinProvider
+ .WithChild(testSkinProvider
+ .WithChild(legacySkinTransformer
+ .WithChild(child)));
+ }
+
+ private bool checkLegacyFruitHyperDashColour(DrawableFruit fruit, Color4 expectedColour) =>
+ fruit.ChildrenOfType().First().Drawable.ChildrenOfType().Any(c => c.Colour == expectedColour);
+
+ private class TestSkin : LegacySkin
+ {
+ public Color4 HyperDashColour
+ {
+ get => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()];
+ set => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()] = value;
+ }
+
+ public Color4 HyperDashAfterImageColour
+ {
+ get => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()];
+ set => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()] = value;
+ }
+
+ public Color4 HyperDashFruitColour
+ {
+ get => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()];
+ set => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()] = value;
+ }
+
+ public TestSkin()
+ : base(new SkinInfo(), null, null, string.Empty)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs
new file mode 100644
index 0000000000..269e783899
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.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.Collections.Generic;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneJuiceStream : TestSceneCatchPlayer
+ {
+ [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 = CatchPlayfield.CENTER_X,
+ Path = new SliderPath(PathType.Linear, new[]
+ {
+ Vector2.Zero,
+ new Vector2(0, 100)
+ }),
+ StartTime = 200
+ },
+ new Banana
+ {
+ X = CatchPlayfield.CENTER_X,
+ 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..7c0b73e8c3 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,9 +2,9 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
index 90a6e609f0..145a40f5f5 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
@@ -5,7 +5,6 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using System.Collections.Generic;
using System.Linq;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Objects;
using osu.Framework.Extensions.IEnumerableExtensions;
@@ -28,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
switch (obj)
{
- case IHasCurve curveData:
+ case IHasPathWithRepeats curveData:
return new JuiceStream
{
StartTime = obj.StartTime,
@@ -36,13 +35,13 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
Path = curveData.Path,
NodeSamples = curveData.NodeSamples,
RepeatCount = curveData.RepeatCount,
- X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH,
+ X = positionData?.X ?? 0,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0
}.Yield();
- case IHasEndTime endTime:
+ case IHasDuration endTime:
return new BananaShower
{
StartTime = obj.StartTime,
@@ -59,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
Samples = obj.Samples,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
- X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH
+ X = positionData?.X ?? 0
}.Yield();
}
}
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index db52fbac1b..bb14988414 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++;
}
}
@@ -62,7 +65,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
case BananaShower bananaShower:
foreach (var banana in bananaShower.NestedHitObjects.OfType())
{
- banana.XOffset = (float)rng.NextDouble();
+ banana.XOffset = (float)(rng.NextDouble() * CatchPlayfield.WIDTH);
rng.Next(); // osu!stable retrieved a random banana type
rng.Next(); // osu!stable retrieved a random banana rotation
rng.Next(); // osu!stable retrieved a random banana colour
@@ -71,13 +74,19 @@ 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;
+
+ // 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;
catchObject.XOffset = 0;
if (catchObject is TinyDroplet)
- catchObject.XOffset = Math.Clamp(rng.Next(-20, 20) / CatchPlayfield.BASE_WIDTH, -catchObject.X, 1 - catchObject.X);
+ catchObject.XOffset = Math.Clamp(rng.Next(-20, 20), -catchObject.X, CatchPlayfield.WIDTH - catchObject.X);
else if (catchObject is Droplet)
rng.Next(); // osu!stable retrieved a random droplet rotation
}
@@ -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) < timeDiff / 3)
applyOffset(ref offsetPosition, positionDiff);
hitObject.XOffset = offsetPosition - hitObject.X;
@@ -145,12 +149,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
private static void applyRandomOffset(ref float position, double maxOffset, FastRandom rng)
{
bool right = rng.NextBool();
- float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset))) / CatchPlayfield.BASE_WIDTH;
+ float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset)));
if (right)
{
// Clamp to the right bound
- if (position + rand <= 1)
+ if (position + rand <= CatchPlayfield.WIDTH)
position += rand;
else
position -= rand;
@@ -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 = Catcher.CalculateCatchWidth(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 dc8df28e6a..9437023c70 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -21,9 +21,13 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System;
+using osu.Framework.Testing;
+using osu.Game.Rulesets.Catch.Skinning;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
{
+ [ExcludeFromDynamicCompile]
public class CatchRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableCatchRuleset(this, beatmap, mods);
@@ -46,7 +50,7 @@ namespace osu.Game.Rulesets.Catch
new KeyBinding(InputKey.Shift, CatchAction.Dash),
};
- public override IEnumerable ConvertLegacyMods(LegacyMods mods)
+ public override IEnumerable ConvertFromLegacyMods(LegacyMods mods)
{
if (mods.HasFlag(LegacyMods.Nightcore))
yield return new CatchModNightcore();
@@ -135,10 +139,14 @@ namespace osu.Game.Rulesets.Catch
public override string ShortName => SHORT_NAME;
+ public override string PlayingVerb => "Catching fruit";
+
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch };
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
+ public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => 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..a317ef252d 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -19,10 +19,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchDifficultyCalculator : DifficultyCalculator
{
- private const double star_scaling_factor = 0.145;
+ private const double star_scaling_factor = 0.153;
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,24 @@ 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(),
- };
+ halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
+
+ // For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay.
+ halfCatcherWidth *= 1 - (Math.Max(0, beatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5.5f) * 0.0625f);
+
+ return new Skill[]
+ {
+ new Movement(halfCatcherWidth),
+ };
+ }
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
{
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
index a6283eb7c4..2ee7cea645 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
@@ -4,12 +4,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
-using osu.Game.Scoring.Legacy;
namespace osu.Game.Rulesets.Catch.Difficulty
{
@@ -34,11 +34,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
mods = Score.Mods;
- fruitsHit = Score?.GetCount300() ?? Score.Statistics[HitResult.Perfect];
- ticksHit = Score?.GetCount100() ?? 0;
- tinyTicksHit = Score?.GetCount50() ?? 0;
- tinyTicksMissed = Score?.GetCountKatu() ?? 0;
- misses = Score.Statistics[HitResult.Miss];
+ fruitsHit = Score.Statistics.GetOrDefault(HitResult.Perfect);
+ ticksHit = Score.Statistics.GetOrDefault(HitResult.LargeTickHit);
+ tinyTicksHit = Score.Statistics.GetOrDefault(HitResult.SmallTickHit);
+ tinyTicksMissed = Score.Statistics.GetOrDefault(HitResult.SmallTickMiss);
+ misses = Score.Statistics.GetOrDefault(HitResult.Miss);
// Don't count scores made with supposedly unranked mods
if (mods.Any(m => !m.Ranked))
@@ -52,8 +52,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
// Longer maps are worth more
double lengthBonus =
- 0.95 + 0.4 * Math.Min(1.0, numTotalHits / 3000.0) +
- (numTotalHits > 3000 ? Math.Log10(numTotalHits / 3000.0) * 0.5 : 0.0);
+ 0.95 + 0.3 * Math.Min(1.0, numTotalHits / 2500.0) +
+ (numTotalHits > 2500 ? Math.Log10(numTotalHits / 2500.0) * 0.475 : 0.0);
// Longer maps are worth more
value *= lengthBonus;
@@ -63,19 +63,28 @@ namespace osu.Game.Rulesets.Catch.Difficulty
// Combo scaling
if (Attributes.MaxCombo > 0)
- value *= Math.Min(Math.Pow(Attributes.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
+ value *= Math.Min(Math.Pow(Score.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
+ double approachRate = Attributes.ApproachRate;
double approachRateFactor = 1.0;
- if (Attributes.ApproachRate > 9.0)
- approachRateFactor += 0.1 * (Attributes.ApproachRate - 9.0); // 10% for each AR above 9
- else if (Attributes.ApproachRate < 8.0)
- approachRateFactor += 0.025 * (8.0 - Attributes.ApproachRate); // 2.5% for each AR below 8
+ if (approachRate > 9.0)
+ approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9
+ if (approachRate > 10.0)
+ approachRateFactor += 0.1 * (approachRate - 10.0); // Additional 10% at AR 11, 30% total
+ else if (approachRate < 8.0)
+ approachRateFactor += 0.025 * (8.0 - approachRate); // 2.5% for each AR below 8
value *= approachRateFactor;
if (mods.Any(m => m is ModHidden))
- // Hiddens gives nothing on max approach rate, and more the lower it is
+ {
value *= 1.05 + 0.075 * (10.0 - Math.Min(10.0, Attributes.ApproachRate)); // 7.5% for each AR below 10
+ // Hiddens gives almost nothing on max approach rate, and more the lower it is
+ if (approachRate <= 10.0)
+ value *= 1.05 + 0.075 * (10.0 - approachRate); // 7.5% for each AR below 10
+ else if (approachRate > 10.0)
+ value *= 1.01 + 0.04 * (11.0 - Math.Min(11.0, approachRate)); // 5% at AR 10, 1% at AR 11
+ }
if (mods.Any(m => m is ModFlashlight))
// Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps.
@@ -91,7 +100,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
return value;
}
- private float accuracy() => totalHits() == 0 ? 0 : Math.Clamp((float)totalSuccessfulHits() / totalHits(), 0, 1);
+ private double accuracy() => totalHits() == 0 ? 0 : Math.Clamp((double)totalSuccessfulHits() / totalHits(), 0, 1);
private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed;
private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit;
private int totalComboHits() => misses + ticksHit + fruitsHit;
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
index 24e526ed19..3e21b8fbaf 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
@@ -3,7 +3,6 @@
using System;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
@@ -21,21 +20,24 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
public readonly float LastNormalizedPosition;
///
- /// Milliseconds elapsed since the start time of the previous , with a minimum of 25ms.
+ /// Milliseconds elapsed since the start time of the previous , with a minimum of 40ms.
///
public readonly double StrainTime;
+ public readonly double ClockRate;
+
public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth)
: base(hitObject, lastObject, clockRate)
{
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
var scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
- NormalizedPosition = BaseObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
- LastNormalizedPosition = LastObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
+ NormalizedPosition = BaseObject.X * scalingFactor;
+ LastNormalizedPosition = LastObject.X * scalingFactor;
- // Every strain interval is hard capped at the equivalent of 600 BPM streaming speed as a safety measure
- StrainTime = Math.Max(25, DeltaTime);
+ // Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
+ StrainTime = Math.Max(40, DeltaTime);
+ ClockRate = clockRate;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
index 7cd569035b..e679231638 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -3,7 +3,6 @@
using System;
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
@@ -13,22 +12,29 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{
private const float absolute_player_positioning_error = 16f;
private const float normalized_hitobject_radius = 41.0f;
- private const double direction_change_bonus = 12.5;
+ private const double direction_change_bonus = 21.0;
- protected override double SkillMultiplier => 850;
+ protected override double SkillMultiplier => 900;
protected override double StrainDecayBase => 0.2;
protected override double DecayWeight => 0.94;
+ protected readonly float HalfCatcherWidth;
+
private float? lastPlayerPosition;
private float lastDistanceMoved;
+ private double lastStrainTime;
+
+ public Movement(float halfCatcherWidth)
+ {
+ HalfCatcherWidth = halfCatcherWidth;
+ }
protected override double StrainValueOf(DifficultyHitObject current)
{
var catchCurrent = (CatchDifficultyHitObject)current;
- if (lastPlayerPosition == null)
- lastPlayerPosition = catchCurrent.LastNormalizedPosition;
+ lastPlayerPosition ??= catchCurrent.LastNormalizedPosition;
float playerPosition = Math.Clamp(
lastPlayerPosition.Value,
@@ -38,47 +44,47 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
float distanceMoved = playerPosition - lastPlayerPosition.Value;
- double distanceAddition = Math.Pow(Math.Abs(distanceMoved), 1.3) / 500;
- double sqrtStrain = Math.Sqrt(catchCurrent.StrainTime);
+ double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catchCurrent.ClockRate);
- double bonus = 0;
+ double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
+ double sqrtStrain = Math.Sqrt(weightedStrainTime);
- // Direction changes give an extra point!
+ double edgeDashBonus = 0;
+
+ // Direction change bonus.
if (Math.Abs(distanceMoved) > 0.1)
{
if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved))
{
- double bonusFactor = Math.Min(absolute_player_positioning_error, Math.Abs(distanceMoved)) / absolute_player_positioning_error;
+ double bonusFactor = Math.Min(50, Math.Abs(distanceMoved)) / 50;
+ double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(lastDistanceMoved)) / 70, 0.38);
- distanceAddition += direction_change_bonus / sqrtStrain * bonusFactor;
-
- // Bonus for tougher direction switches and "almost" hyperdashes at this point
- if (catchCurrent.LastObject.DistanceToHyperDash <= 10 / CatchPlayfield.BASE_WIDTH)
- bonus = 0.3 * bonusFactor;
+ distanceAddition += direction_change_bonus / Math.Sqrt(lastStrainTime + 16) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0);
}
// Base bonus for every movement, giving some weight to streams.
- distanceAddition += 7.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain;
+ distanceAddition += 12.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain;
}
- // Bonus for "almost" hyperdashes at corner points
- if (catchCurrent.LastObject.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH)
+ // Bonus for edge dashes.
+ if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
{
if (!catchCurrent.LastObject.HyperDash)
- bonus += 1.0;
+ edgeDashBonus += 5.7;
else
{
// After a hyperdash we ARE in the correct position. Always!
playerPosition = catchCurrent.NormalizedPosition;
}
- distanceAddition *= 1.0 + bonus * ((10 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 10);
+ distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
}
lastPlayerPosition = playerPosition;
lastDistanceMoved = distanceMoved;
+ lastStrainTime = catchCurrent.StrainTime;
- return distanceAddition / catchCurrent.StrainTime;
+ return distanceAddition / weightedStrainTime;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
index 374dd50c11..a7449ba4e1 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
return 0;
case HitResult.Perfect:
- return 0.008;
+ return DEFAULT_MAX_HEALTH_INCREASE * 0.75;
}
}
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
index f1399bb5c0..e87ecba749 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
@@ -18,17 +18,5 @@ namespace osu.Game.Rulesets.Catch.Judgements
return 30;
}
}
-
- protected override double HealthIncreaseFor(HitResult result)
- {
- switch (result)
- {
- default:
- return base.HealthIncreaseFor(result);
-
- case HitResult.Perfect:
- return 0.007;
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
index 8fd9ac92ba..2149ed9712 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
@@ -23,18 +23,6 @@ namespace osu.Game.Rulesets.Catch.Judgements
}
}
- protected override double HealthIncreaseFor(HitResult result)
- {
- switch (result)
- {
- default:
- return -0.02;
-
- case HitResult.Perfect:
- return 0.01;
- }
- }
-
///
/// Whether fruit on the platter should explode or drop.
/// Note that this is only checked if the owning object is also
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
index 3829b5e94f..d607b49ea4 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
return 0;
case HitResult.Perfect:
- return 0.004;
+ return 0.02;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
index 8377b3786a..acdd0a420c 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.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.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
@@ -10,7 +11,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 +21,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,
@@ -30,6 +31,22 @@ namespace osu.Game.Rulesets.Catch.Mods
Value = 5,
};
+ public override string SettingDescription
+ {
+ get
+ {
+ string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
+ string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
+
+ return string.Join(", ", new[]
+ {
+ circleSize,
+ base.SettingDescription,
+ approachRate
+ }.Where(s => !string.IsNullOrEmpty(s)));
+ }
+ }
+
protected override void TransferSettings(BeatmapDifficulty difficulty)
{
base.TransferSettings(difficulty);
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 a47efcc10a..c1d24395e4 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
@@ -9,22 +9,31 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Play;
using osuTK;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset
+ public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset, IApplicableToPlayer
{
public override string Description => @"Use the mouse to control the catcher.";
+ private DrawableRuleset drawableRuleset;
+
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
- drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield));
+ this.drawableRuleset = drawableRuleset;
+ }
+
+ public void ApplyToPlayer(Player player)
+ {
+ if (!drawableRuleset.HasReplayLoaded.Value)
+ drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield));
}
private class MouseInputHelper : Drawable, IKeyBindingHandler, IRequireHighFrequencyMousePosition
{
- private readonly CatcherArea.Catcher catcher;
+ private readonly Catcher catcher;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
@@ -34,9 +43,12 @@ namespace osu.Game.Rulesets.Catch.Mods
RelativeSizeAxes = Axes.Both;
}
- //disable keyboard controls
+ // disable keyboard controls
public bool OnPressed(CatchAction action) => true;
- public bool OnReleased(CatchAction action) => true;
+
+ public void OnReleased(CatchAction action)
+ {
+ }
protected override bool OnMouseMove(MouseMoveEvent e)
{
diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
index 267e6d12c7..04a995c77e 100644
--- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
@@ -1,23 +1,27 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Threading;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects
{
- public class BananaShower : CatchHitObject, IHasEndTime
+ public class BananaShower : CatchHitObject, IHasDuration
{
public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
public override bool LastInCombo => true;
- protected override void CreateNestedHitObjects()
+ public override Judgement CreateJudgement() => new IgnoreJudgement();
+
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
- base.CreateNestedHitObjects();
- createBananas();
+ base.CreateNestedHitObjects(cancellationToken);
+ createBananas(cancellationToken);
}
- private void createBananas()
+ private void createBananas(CancellationToken cancellationToken)
{
double spacing = Duration;
while (spacing > 100)
@@ -28,6 +32,8 @@ namespace osu.Game.Rulesets.Catch.Objects
for (double i = StartTime; i <= EndTime; i += spacing)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
AddNested(new Banana
{
Samples = Samples,
@@ -36,7 +42,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..04932ecdbb 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -5,6 +5,7 @@ using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Beatmaps;
+using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@@ -13,10 +14,13 @@ 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;
+ ///
+ /// The horizontal position of the fruit between 0 and .
+ ///
public float X
{
get => x + XOffset;
@@ -32,7 +36,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 +94,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 +104,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 958cd19d50..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.MathUtils;
-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 66%
rename from osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs
rename to osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
index b7c05392f3..c6345a9df7 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
@@ -2,24 +2,52 @@
// 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 osu.Game.Rulesets.Catch.UI;
+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,10 +69,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
public virtual bool StaysOnPlate => CanBePlated;
+ public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
+
+ protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH;
+
protected DrawableCatchHitObject(CatchHitObject hitObject)
: base(hitObject)
{
- RelativePositionAxes = Axes.X;
X = hitObject.X;
}
@@ -62,10 +93,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..7ac9f11ad6
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
@@ -0,0 +1,117 @@
+// 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.Catch.UI;
+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 = Catcher.DEFAULT_HYPER_DASH_COLOUR,
+ BorderThickness = 12f * RADIUS_ADJUST,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ AlwaysPresent = true,
+ Alpha = 0.3f,
+ Blending = BlendingParameters.Additive,
+ RelativeSizeAxes = Axes.Both,
+ Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
+ }
+ }
+ });
+ }
+ }
+
+ 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..6b8b70ed54 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -3,26 +3,29 @@
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
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;
namespace osu.Game.Rulesets.Catch.Objects
{
- public class JuiceStream : CatchHitObject, IHasCurve
+ public class JuiceStream : CatchHitObject, IHasPathWithRepeats
{
///
/// Positional distance that results in a duration of one second, before any speed adjustments.
///
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 .
@@ -42,11 +45,11 @@ namespace osu.Game.Rulesets.Catch.Objects
TickDistance = scoringDistance / difficulty.SliderTickRate;
}
- protected override void CreateNestedHitObjects()
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
- base.CreateNestedHitObjects();
+ base.CreateNestedHitObjects(cancellationToken);
- var tickSamples = Samples.Select(s => new HitSampleInfo
+ var dropletSamples = Samples.Select(s => new HitSampleInfo
{
Bank = s.Bank,
Name = @"slidertick",
@@ -55,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Objects
SliderEventDescriptor? lastEvent = null;
- foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset))
+ foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
{
// generate tiny droplets since the last point
if (lastEvent != null)
@@ -70,12 +73,13 @@ namespace osu.Game.Rulesets.Catch.Objects
for (double t = timeBetweenTiny; t < sinceLastTick; t += timeBetweenTiny)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
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,
});
}
}
@@ -90,9 +94,9 @@ 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,
+ X = X + Path.PositionAt(e.PathProgress).X,
});
break;
@@ -103,18 +107,22 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = Samples,
StartTime = e.Time,
- X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
+ X = X + Path.PositionAt(e.PathProgress).X,
});
break;
}
}
}
- public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
+ public float EndX => X + this.CurvePositionAt(1).X;
- public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH;
+ public double Duration
+ {
+ get => 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;
+ public double EndTime => StartTime + Duration;
private readonly SliderPath path = new SliderPath();
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
index 6c8515eb90..5d11c574b1 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
@@ -3,7 +3,7 @@
using System;
using System.Linq;
-using osu.Framework.MathUtils;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.Catch.Beatmaps;
@@ -32,9 +32,9 @@ 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;
+ float lastPosition = CatchPlayfield.CENTER_X;
double lastTime = 0;
void moveToNext(CatchHitObject h)
@@ -51,11 +51,11 @@ namespace osu.Game.Rulesets.Catch.Replays
bool impossibleJump = speedRequired > movement_speed * 2;
// todo: get correct catcher size, based on difficulty CS.
- const float catcher_width_half = CatcherArea.CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * 0.3f * 0.5f;
+ const float catcher_width_half = CatcherArea.CATCHER_SIZE * 0.3f * 0.5f;
if (lastPosition - catcher_width_half < h.X && lastPosition + catcher_width_half > h.X)
{
- //we are already in the correct range.
+ // we are already in the correct range.
lastTime = h.StartTime;
addFrame(h.StartTime, lastPosition);
return;
@@ -72,14 +72,14 @@ namespace osu.Game.Rulesets.Catch.Replays
}
else if (dashRequired)
{
- //we do a movement in two parts - the dash part then the normal part...
+ // we do a movement in two parts - the dash part then the normal part...
double timeAtNormalSpeed = positionChange / movement_speed;
double timeWeNeedToSave = timeAtNormalSpeed - timeAvailable;
double timeAtDashSpeed = timeWeNeedToSave / 2;
float midPosition = (float)Interpolation.Lerp(lastPosition, h.X, (float)timeAtDashSpeed / timeAvailable);
- //dash movement
+ // dash movement
addFrame(h.StartTime - timeAvailable + 1, lastPosition, true);
addFrame(h.StartTime - timeAvailable + timeAtDashSpeed, midPosition);
addFrame(h.StartTime, h.X);
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
index 22532bc9ec..f122588a2b 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
@@ -5,7 +5,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Input.StateChanges;
-using osu.Framework.MathUtils;
+using osu.Framework.Utils;
using osu.Game.Replays;
using osu.Game.Rulesets.Replays;
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs
index b41a5e0612..7efd832f62 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs
@@ -4,7 +4,6 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
@@ -39,9 +38,9 @@ namespace osu.Game.Rulesets.Catch.Replays
}
}
- public void ConvertFrom(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
+ public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
{
- Position = currentFrame.Position.X / CatchPlayfield.BASE_WIDTH;
+ Position = currentFrame.Position.X;
Dashing = currentFrame.ButtonState == ReplayButtonState.Left1;
if (Dashing)
@@ -56,5 +55,14 @@ namespace osu.Game.Rulesets.Catch.Replays
Actions.Add(CatchAction.MoveLeft);
}
}
+
+ public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
+ {
+ ReplayButtonState state = ReplayButtonState.None;
+
+ if (Actions.Contains(CatchAction.Dash)) state |= ReplayButtonState.Left1;
+
+ return new LegacyReplayFrame(Time, Position, null, state);
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
new file mode 100644
index 0000000000..d929da1a29
--- /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.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Skinning
+{
+ public class CatchLegacySkinTransformer : LegacySkinTransformer
+ {
+ public CatchLegacySkinTransformer(ISkinSource source)
+ : base(source)
+ {
+ }
+
+ public override 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 override IBindable GetConfig(TLookup lookup)
+ {
+ switch (lookup)
+ {
+ case CatchSkinColour colour:
+ return Source.GetConfig(new SkinCustomColourLookup(colour));
+ }
+
+ return Source.GetConfig(lookup);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs
new file mode 100644
index 0000000000..4506111498
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs
@@ -0,0 +1,23 @@
+// 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.Skinning
+{
+ public enum CatchSkinColour
+ {
+ ///
+ /// The colour to be used for the catcher while in hyper-dashing state.
+ ///
+ HyperDash,
+
+ ///
+ /// The colour to be used for fruits that grant the catcher the ability to hyper-dash.
+ ///
+ HyperDashFruit,
+
+ ///
+ /// The colour to be used for the "exploding" catcher sprite on beginning of hyper-dashing.
+ ///
+ HyperDashAfterImage,
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
new file mode 100644
index 0000000000..5be54d3882
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
@@ -0,0 +1,82 @@
+// 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.Catch.UI;
+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
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Blending = BlendingParameters.Additive,
+ Depth = 1,
+ Alpha = 0.7f,
+ Scale = new Vector2(1.2f),
+ Texture = skin.GetTexture(lookupName),
+ Colour = skin.GetConfig(CatchSkinColour.HyperDashFruit)?.Value ??
+ skin.GetConfig(CatchSkinColour.HyperDash)?.Value ??
+ Catcher.DEFAULT_HYPER_DASH_COLOUR,
+ };
+
+ 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..154e1576db 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;
@@ -16,30 +16,44 @@ namespace osu.Game.Rulesets.Catch.UI
{
public class CatchPlayfield : ScrollingPlayfield
{
- public const float BASE_WIDTH = 512;
+ ///
+ /// The width of the playfield.
+ /// The horizontal movement of the catcher is confined in the area of this width.
+ ///
+ public const float WIDTH = 512;
+
+ ///
+ /// The center position of the playfield.
+ ///
+ public const float CENTER_X = WIDTH / 2;
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)
{
- Container explodingFruitContainer;
-
- InternalChildren = new Drawable[]
+ var explodingFruitContainer = new Container
{
- explodingFruitContainer = new Container
- {
- RelativeSizeAxes = Axes.Both,
- },
- CatcherArea = new CatcherArea(difficulty)
- {
- CreateDrawableRepresentation = createDrawableRepresentation,
- ExplodingFruitTarget = explodingFruitContainer,
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.TopLeft,
- },
- HitObjectContainer
+ RelativeSizeAxes = Axes.Both,
+ };
+
+ CatcherArea = new CatcherArea(difficulty)
+ {
+ CreateDrawableRepresentation = createDrawableRepresentation,
+ ExplodingFruitTarget = explodingFruitContainer,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.TopLeft,
+ };
+
+ InternalChildren = new[]
+ {
+ explodingFruitContainer,
+ CatcherArea.MovableCatcher.CreateProxiedContent(),
+ HitObjectContainer,
+ CatcherArea
};
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
index b8d3dc9017..8ee23461ba 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
base.Update();
- Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.BASE_WIDTH);
+ Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.WIDTH);
Size = Vector2.Divide(Vector2.One, Scale);
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs b/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs
new file mode 100644
index 0000000000..9a4d1f9585
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.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 System.Collections.Generic;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Catch.Replays;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ public class CatchReplayRecorder : ReplayRecorder
+ {
+ private readonly CatchPlayfield playfield;
+
+ public CatchReplayRecorder(Replay target, CatchPlayfield playfield)
+ : base(target)
+ {
+ this.playfield = playfield;
+ }
+
+ protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame)
+ => new CatchReplayFrame(Time.Current, playfield.CatcherArea.MovableCatcher.X, actions.Contains(CatchAction.Dash), previousFrame as CatchReplayFrame);
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
new file mode 100644
index 0000000000..8820dff730
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -0,0 +1,490 @@
+// 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 JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Animations;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.Skinning;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ public class Catcher : SkinReloadableDrawable, IKeyBindingHandler
+ {
+ ///
+ /// The default colour used to tint hyper-dash fruit, along with the moving catcher, its trail
+ /// and end glow/after-image during a hyper-dash.
+ ///
+ public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red;
+
+ ///
+ /// The duration between transitioning to hyper-dash state.
+ ///
+ public const double HYPER_DASH_TRANSITION_DURATION = 180;
+
+ ///
+ /// 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;
+
+ public Container ExplodingFruitTarget;
+
+ private Container caughtFruitContainer { get; } = new Container
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.BottomCentre,
+ };
+
+ [NotNull]
+ private readonly Container trailsTarget;
+
+ private CatcherTrailDisplay trails;
+
+ public CatcherAnimationState CurrentState { get; private set; }
+
+ ///
+ /// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable.
+ ///
+ private const float allowed_catch_range = 0.8f;
+
+ ///
+ /// The drawable catcher for .
+ ///
+ internal Drawable CurrentDrawableCatcher => currentCatcher.Drawable;
+
+ private bool dashing;
+
+ public bool Dashing
+ {
+ get => dashing;
+ protected set
+ {
+ if (value == dashing) return;
+
+ dashing = value;
+
+ updateTrailVisibility();
+ }
+ }
+
+ ///
+ /// Width of the area that can be used to attempt catches during gameplay.
+ ///
+ private readonly float catchWidth;
+
+ private CatcherSprite catcherIdle;
+ private CatcherSprite catcherKiai;
+ private CatcherSprite catcherFail;
+
+ private CatcherSprite currentCatcher;
+
+ private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
+ private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
+
+ private int currentDirection;
+
+ private double hyperDashModifier = 1;
+ private int hyperDashDirection;
+ private float hyperDashTargetPosition;
+ private Bindable hitLighting;
+
+ public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null)
+ {
+ this.trailsTarget = trailsTarget;
+
+ Origin = Anchor.TopCentre;
+
+ Size = new Vector2(CatcherArea.CATCHER_SIZE);
+ if (difficulty != null)
+ Scale = calculateScale(difficulty);
+
+ catchWidth = CalculateCatchWidth(Scale);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ hitLighting = config.GetBindable(OsuSetting.HitLighting);
+
+ InternalChildren = new Drawable[]
+ {
+ caughtFruitContainer,
+ 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,
+ }
+ };
+
+ trailsTarget.Add(trails = new CatcherTrailDisplay(this));
+
+ updateCatcher();
+ }
+
+ ///
+ /// Creates proxied content to be displayed beneath hitobjects.
+ ///
+ public Drawable CreateProxiedContent() => caughtFruitContainer.CreateProxy();
+
+ ///
+ /// Calculates the scale of the catcher based off the provided beatmap difficulty.
+ ///
+ private static Vector2 calculateScale(BeatmapDifficulty difficulty)
+ => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
+
+ ///
+ /// Calculates the width of the area used for attempting catches in gameplay.
+ ///
+ /// The scale of the catcher.
+ internal static float CalculateCatchWidth(Vector2 scale)
+ => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * allowed_catch_range;
+
+ ///
+ /// Calculates the width of the area used for attempting catches in gameplay.
+ ///
+ /// The beatmap difficulty.
+ internal static float CalculateCatchWidth(BeatmapDifficulty difficulty)
+ => CalculateCatchWidth(calculateScale(difficulty));
+
+ ///
+ /// 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 = 10;
+
+ while (caughtFruitContainer.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) * diff * 2;
+ fruit.Y -= RNG.NextSingle() * diff;
+ }
+
+ fruit.X = Math.Clamp(fruit.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
+
+ caughtFruitContainer.Add(fruit);
+
+ if (hitLighting.Value)
+ {
+ AddInternal(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;
+ var catcherPosition = Position.X;
+
+ 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 - 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)
+ {
+ var wasHyperDashing = HyperDashing;
+
+ if (modifier <= 1 || X == targetPosition)
+ {
+ hyperDashModifier = 1;
+ hyperDashDirection = 0;
+
+ if (wasHyperDashing)
+ runHyperDashStateTransition(false);
+ }
+ else
+ {
+ hyperDashModifier = modifier;
+ hyperDashDirection = Math.Sign(targetPosition - X);
+ hyperDashTargetPosition = targetPosition;
+
+ if (!wasHyperDashing)
+ {
+ trails.DisplayEndGlow();
+ runHyperDashStateTransition(true);
+ }
+ }
+ }
+
+ private void runHyperDashStateTransition(bool hyperDashing)
+ {
+ trails.HyperDashTrailsColour = hyperDashColour;
+ trails.EndGlowSpritesColour = hyperDashEndGlowColour;
+ updateTrailVisibility();
+
+ if (hyperDashing)
+ {
+ this.FadeColour(hyperDashColour, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
+ this.FadeTo(0.2f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
+ }
+ else
+ {
+ this.FadeColour(Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
+ this.FadeTo(1f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
+ }
+ }
+
+ private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing;
+
+ 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, CatchPlayfield.WIDTH);
+
+ 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 caughtFruitContainer.ToArray())
+ Drop(f);
+ }
+
+ ///
+ /// Explode any fruit off the plate.
+ ///
+ public void Explode()
+ {
+ foreach (var f in caughtFruitContainer.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 SkinChanged(ISkinSource skin, bool allowFallback)
+ {
+ base.SkinChanged(skin, allowFallback);
+
+ hyperDashColour =
+ skin.GetConfig(CatchSkinColour.HyperDash)?.Value ??
+ DEFAULT_HYPER_DASH_COLOUR;
+
+ hyperDashEndGlowColour =
+ skin.GetConfig(CatchSkinColour.HyperDashAfterImage)?.Value ??
+ hyperDashColour;
+
+ runHyperDashStateTransition(HyperDashing);
+ }
+
+ 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 IFramedAnimation)?.GotoFrame(0);
+ }
+
+ private void updateState(CatcherAnimationState state)
+ {
+ if (CurrentState == state)
+ return;
+
+ CurrentState = state;
+ updateCatcher();
+ }
+
+ private void removeFromPlateWithTransform(DrawableHitObject fruit, Action action)
+ {
+ if (ExplodingFruitTarget != null)
+ {
+ fruit.Anchor = Anchor.TopLeft;
+ fruit.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
+
+ if (!caughtFruitContainer.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 2d6ce02e45..4255c3b1af 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.MathUtils;
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,29 +20,28 @@ namespace osu.Game.Rulesets.Catch.UI
{
public const float CATCHER_SIZE = 106.75f;
- protected internal readonly Catcher MovableCatcher;
-
public Func> CreateDrawableRepresentation;
+ public readonly Catcher MovableCatcher;
+
public Container ExplodingFruitTarget
{
set => MovableCatcher.ExplodingFruitTarget = value;
}
+ private DrawableCatchHitObject lastPlateableFruit;
+
public CatcherArea(BeatmapDifficulty difficulty = null)
{
- RelativeSizeAxes = Axes.X;
- Height = CATCHER_SIZE;
- Child = MovableCatcher = new Catcher(difficulty)
- {
- AdditiveTarget = this,
- };
+ Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
+ Child = MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X };
}
- private DrawableCatchHitObject lastPlateableFruit;
-
public void OnResult(DrawableCatchHitObject fruit, JudgementResult result)
{
+ if (result.Judgement is IgnoreJudgement)
+ return;
+
void runAfterLoaded(Action action)
{
if (lastPlateableFruit == null)
@@ -63,6 +57,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 +68,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 +81,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();
@@ -102,366 +106,5 @@ namespace osu.Game.Rulesets.Catch.UI
if (state?.CatcherX != null)
MovableCatcher.X = state.CatcherX.Value;
}
-
- public bool OnReleased(CatchAction action) => false;
-
- 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 bool OnReleased(CatchAction action)
- {
- switch (action)
- {
- case CatchAction.MoveLeft:
- currentDirection++;
- return true;
-
- case CatchAction.MoveRight:
- currentDirection--;
- return true;
-
- case CatchAction.Dash:
- Dashing = false;
- return true;
- }
-
- return false;
- }
-
- ///
- /// 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;
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
index 025fa9c56e..ef69e3d2d1 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.ScaleToFit)
{
+ 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/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
new file mode 100644
index 0000000000..bab3cb748b
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
@@ -0,0 +1,135 @@
+// 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 JetBrains.Annotations;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Animations;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ ///
+ /// Represents a component responsible for displaying
+ /// the appropriate catcher trails when requested to.
+ ///
+ public class CatcherTrailDisplay : CompositeDrawable
+ {
+ private readonly Catcher catcher;
+
+ private readonly Container dashTrails;
+ private readonly Container hyperDashTrails;
+ private readonly Container endGlowSprites;
+
+ private Color4 hyperDashTrailsColour;
+
+ public Color4 HyperDashTrailsColour
+ {
+ get => hyperDashTrailsColour;
+ set
+ {
+ if (hyperDashTrailsColour == value)
+ return;
+
+ hyperDashTrailsColour = value;
+ hyperDashTrails.FadeColour(hyperDashTrailsColour, Catcher.HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
+ }
+ }
+
+ private Color4 endGlowSpritesColour;
+
+ public Color4 EndGlowSpritesColour
+ {
+ get => endGlowSpritesColour;
+ set
+ {
+ if (endGlowSpritesColour == value)
+ return;
+
+ endGlowSpritesColour = value;
+ endGlowSprites.FadeColour(endGlowSpritesColour, Catcher.HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
+ }
+ }
+
+ private bool trail;
+
+ ///
+ /// Whether to start displaying trails following the catcher.
+ ///
+ public bool DisplayTrail
+ {
+ get => trail;
+ set
+ {
+ if (trail == value)
+ return;
+
+ trail = value;
+
+ if (trail)
+ displayTrail();
+ }
+ }
+
+ public CatcherTrailDisplay([NotNull] Catcher catcher)
+ {
+ this.catcher = catcher ?? throw new ArgumentNullException(nameof(catcher));
+
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new[]
+ {
+ dashTrails = new Container { RelativeSizeAxes = Axes.Both },
+ hyperDashTrails = new Container { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
+ endGlowSprites = new Container { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
+ };
+ }
+
+ ///
+ /// Displays a single end-glow catcher sprite.
+ ///
+ public void DisplayEndGlow()
+ {
+ var endGlow = createTrailSprite(endGlowSprites);
+
+ endGlow.MoveToOffset(new Vector2(0, -10), 1200, Easing.In);
+ endGlow.ScaleTo(endGlow.Scale * 0.95f).ScaleTo(endGlow.Scale * 1.2f, 1200, Easing.In);
+ endGlow.FadeOut(1200);
+ endGlow.Expire(true);
+ }
+
+ private void displayTrail()
+ {
+ if (!DisplayTrail)
+ return;
+
+ var sprite = createTrailSprite(catcher.HyperDashing ? hyperDashTrails : dashTrails);
+
+ sprite.FadeTo(0.4f).FadeOut(800, Easing.OutQuint);
+ sprite.Expire(true);
+
+ Scheduler.AddDelayed(displayTrail, catcher.HyperDashing ? 25 : 50);
+ }
+
+ private CatcherTrailSprite createTrailSprite(Container target)
+ {
+ var texture = (catcher.CurrentDrawableCatcher as TextureAnimation)?.CurrentFrame ?? ((Sprite)catcher.CurrentDrawableCatcher).Texture;
+
+ var sprite = new CatcherTrailSprite(texture)
+ {
+ Anchor = catcher.Anchor,
+ Scale = catcher.Scale,
+ Blending = BlendingParameters.Additive,
+ RelativePositionAxes = catcher.RelativePositionAxes,
+ Position = catcher.Position
+ };
+
+ target.Add(sprite);
+
+ return sprite;
+ }
+ }
+}
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..ebe45aa3ab 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;
@@ -32,6 +32,8 @@ namespace osu.Game.Rulesets.Catch.UI
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
+ protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new CatchReplayRecorder(replay, (CatchPlayfield)Playfield);
+
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty, CreateDrawableRepresentation);
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer();
diff --git a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
similarity index 83%
rename from osu.Game.Rulesets.Mania/UI/HitExplosion.cs
rename to osu.Game.Rulesets.Catch/UI/HitExplosion.cs
index ccbff226a9..04a86f83be 100644
--- a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs
+++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
@@ -5,34 +5,30 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
-using osu.Framework.MathUtils;
-using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Rulesets.Mania.UI
+namespace osu.Game.Rulesets.Catch.UI
{
- internal class HitExplosion : CompositeDrawable
+ public class HitExplosion : CompositeDrawable
{
- public override bool RemoveWhenNotAlive => true;
-
private readonly CircularContainer largeFaint;
- private readonly CircularContainer mainGlow1;
- public HitExplosion(Color4 objectColour, bool isSmall = false)
+ public HitExplosion(DrawableCatchHitObject fruit)
{
- RelativeSizeAxes = Axes.X;
- Height = NotePiece.NOTE_HEIGHT;
+ Size = new Vector2(20);
+ Anchor = Anchor.TopCentre;
+ Origin = Anchor.BottomCentre;
+
+ Color4 objectColour = fruit.AccentColour.Value;
// scale roughly in-line with visual appearance of notes
- Scale = new Vector2(1f, 0.6f);
-
- if (isSmall)
- Scale *= 0.5f;
const float angle_variangle = 15; // should be less than 45
- const float roundness = 80;
+ const float roundness = 100;
const float initial_height = 10;
@@ -57,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.UI
Radius = 200,
},
},
- mainGlow1 = new CircularContainer
+ new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -111,17 +107,15 @@ namespace osu.Game.Rulesets.Mania.UI
protected override void LoadComplete()
{
- const double duration = 200;
-
base.LoadComplete();
+ const double duration = 400;
+
largeFaint
.ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
.FadeOut(duration * 2);
- mainGlow1.ScaleTo(1.4f, duration, Easing.OutQuint);
-
- this.FadeOut(duration, Easing.Out);
+ this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out);
Expire(true);
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
index 12865385b6..d0ff1fab43 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
@@ -4,7 +4,7 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
-using osu.Framework.MathUtils;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs
new file mode 100644
index 0000000000..c8feb4ae24
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs
@@ -0,0 +1,85 @@
+// 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.Utils;
+using osu.Game.Audio;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ [TestFixture]
+ public class ManiaBeatmapSampleConversionTest : BeatmapConversionTest, SampleConvertValue>
+ {
+ protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
+
+ [TestCase("convert-samples")]
+ [TestCase("mania-samples")]
+ [TestCase("slider-convert-samples")]
+ public void Test(string name) => base.Test(name);
+
+ protected override IEnumerable CreateConvertValue(HitObject hitObject)
+ {
+ yield return new SampleConvertValue
+ {
+ StartTime = hitObject.StartTime,
+ EndTime = hitObject.GetEndTime(),
+ Column = ((ManiaHitObject)hitObject).Column,
+ Samples = getSampleNames(hitObject.Samples),
+ NodeSamples = getNodeSampleNames((hitObject as HoldNote)?.NodeSamples)
+ };
+ }
+
+ private IList getSampleNames(IList hitSampleInfo)
+ => hitSampleInfo.Select(sample => sample.LookupNames.First()).ToList();
+
+ private IList> getNodeSampleNames(List> hitSampleInfo)
+ => hitSampleInfo?.Select(getSampleNames)
+ .ToList();
+
+ protected override Ruleset CreateRuleset() => new ManiaRuleset();
+ }
+
+ public struct SampleConvertValue : IEquatable
+ {
+ ///
+ /// A sane value to account for osu!stable using ints everywhere.
+ ///
+ private const float conversion_lenience = 2;
+
+ public double StartTime;
+ public double EndTime;
+ public int Column;
+ public IList Samples;
+ public IList> NodeSamples;
+
+ public bool Equals(SampleConvertValue other)
+ => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
+ && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
+ && samplesEqual(Samples, other.Samples)
+ && nodeSamplesEqual(NodeSamples, other.NodeSamples);
+
+ private static bool samplesEqual(ICollection firstSampleList, ICollection secondSampleList)
+ => firstSampleList.SequenceEqual(secondSampleList);
+
+ private static bool nodeSamplesEqual(ICollection> firstSampleList, ICollection> secondSampleList)
+ {
+ if (firstSampleList == null && secondSampleList == null)
+ return true;
+
+ // both items can't be null now, so if any single one is, then they're not equal
+ if (firstSampleList == null || secondSampleList == null)
+ return false;
+
+ return firstSampleList.Count == secondSampleList.Count
+ // cannot use .Zip() without the selector function as it doesn't compile in android test project
+ && firstSampleList.Zip(secondSampleList, (first, second) => (first, second))
+ .All(samples => samples.first.SequenceEqual(samples.second));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaColumnTypeTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaColumnTypeTest.cs
new file mode 100644
index 0000000000..40a6e1fdae
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaColumnTypeTest.cs
@@ -0,0 +1,50 @@
+// 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.Game.Rulesets.Mania.Beatmaps;
+using NUnit.Framework;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ [TestFixture]
+ public class ManiaColumnTypeTest
+ {
+ [TestCase(new[]
+ {
+ ColumnType.Special
+ }, 1)]
+ [TestCase(new[]
+ {
+ ColumnType.Odd,
+ ColumnType.Even,
+ ColumnType.Even,
+ ColumnType.Odd
+ }, 4)]
+ [TestCase(new[]
+ {
+ ColumnType.Odd,
+ ColumnType.Even,
+ ColumnType.Odd,
+ ColumnType.Special,
+ ColumnType.Odd,
+ ColumnType.Even,
+ ColumnType.Odd
+ }, 7)]
+ public void Test(IEnumerable expected, int columns)
+ {
+ var definition = new StageDefinition
+ {
+ Columns = columns
+ };
+ var results = getResults(definition);
+ Assert.AreEqual(expected, results);
+ }
+
+ private IEnumerable getResults(StageDefinition definition)
+ {
+ for (var i = 0; i < definition.Columns; i++)
+ yield return definition.GetTypeOfColumn(i);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs
index 909d0d45c6..9049bb3a82 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
}
- protected override RulesetKeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
+ protected override KeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
=> new LocalKeyBindingContainer(ruleset, variant, unique);
private class LocalKeyBindingContainer : RulesetKeyBindingContainer
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs
new file mode 100644
index 0000000000..40bb83aece
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.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 NUnit.Framework;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Replays;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ [TestFixture]
+ public class ManiaLegacyReplayTest
+ {
+ [TestCase(ManiaAction.Key1)]
+ [TestCase(ManiaAction.Key1, ManiaAction.Key2)]
+ [TestCase(ManiaAction.Special1)]
+ [TestCase(ManiaAction.Key8)]
+ public void TestEncodeDecodeSingleStage(params ManiaAction[] actions)
+ {
+ var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 9 });
+
+ var frame = new ManiaReplayFrame(0, actions);
+ var legacyFrame = frame.ToLegacy(beatmap);
+
+ var decodedFrame = new ManiaReplayFrame();
+ decodedFrame.FromLegacy(legacyFrame, beatmap);
+
+ Assert.That(decodedFrame.Actions, Is.EquivalentTo(frame.Actions));
+ }
+
+ [TestCase(ManiaAction.Key1)]
+ [TestCase(ManiaAction.Key1, ManiaAction.Key2)]
+ [TestCase(ManiaAction.Special1)]
+ [TestCase(ManiaAction.Special2)]
+ [TestCase(ManiaAction.Special1, ManiaAction.Special2)]
+ [TestCase(ManiaAction.Special1, ManiaAction.Key5)]
+ [TestCase(ManiaAction.Key8)]
+ public void TestEncodeDecodeDualStage(params ManiaAction[] actions)
+ {
+ var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 5 });
+ beatmap.Stages.Add(new StageDefinition { Columns = 5 });
+
+ var frame = new ManiaReplayFrame(0, actions);
+ var legacyFrame = frame.ToLegacy(beatmap);
+
+ var decodedFrame = new ManiaReplayFrame();
+ decodedFrame.FromLegacy(legacyFrame, beatmap);
+
+ Assert.That(decodedFrame.Actions, Is.EquivalentTo(frame.Actions));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
index afde1c9521..0fe4a3c669 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
@@ -7,20 +7,18 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
-using osu.Game.Rulesets.Mania.Edit;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
-using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests
{
- [Cached(Type = typeof(IManiaHitObjectComposer))]
- public abstract class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene, IManiaHitObjectComposer
+ public abstract class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene
{
private readonly Column column;
@@ -41,16 +39,20 @@ namespace osu.Game.Rulesets.Mania.Tests
AccentColour = Color4.OrangeRed,
Clock = new FramedClock(new StopwatchClock()), // No scroll
});
+ }
- AddStep("change direction", () => ((ScrollingTestContainer)HitObjectContainer).Flip());
+ protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
+ {
+ var time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
+ var pos = column.ScreenSpacePositionAtTime(time);
+
+ return new SnapResult(pos, time, column);
}
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };
protected override void AddHitObject(DrawableHitObject hitObject) => column.Add((DrawableManiaHitObject)hitObject);
- public Column ColumnAt(Vector2 screenSpacePosition) => column;
-
- public int TotalColumns => 1;
+ public ManiaPlayfield Playfield => null;
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs
index b598893e8c..149f6582ab 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs
@@ -4,25 +4,20 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Timing;
-using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
-using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests
{
- [Cached(Type = typeof(IManiaHitObjectComposer))]
- public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene, IManiaHitObjectComposer
+ public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene
{
[Cached(Type = typeof(IAdjustableClock))]
private readonly IAdjustableClock clock = new StopwatchClock();
- private readonly Column column;
-
protected ManiaSelectionBlueprintTestScene()
{
- Add(column = new Column(0)
+ Add(new Column(0)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -31,8 +26,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
}
- public Column ColumnAt(Vector2 screenSpacePosition) => column;
-
- public int TotalColumns => 1;
+ public ManiaPlayfield Playfield => null;
}
}
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..2e3b21aed7
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs
@@ -0,0 +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 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
+ {
+ protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
+
+ public TestSceneManiaModPerfect()
+ : base(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/Resources/SampleLookups/mania-hitobject-beatmap-custom-sample-bank.osu b/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-custom-sample-bank.osu
new file mode 100644
index 0000000000..4f8e1b68dd
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-custom-sample-bank.osu
@@ -0,0 +1,10 @@
+osu file format v14
+
+[General]
+Mode: 3
+
+[TimingPoints]
+0,300,4,0,2,100,1,0
+
+[HitObjects]
+444,320,1000,5,2,0:0:0:0:
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-normal-sample-bank.osu b/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-normal-sample-bank.osu
new file mode 100644
index 0000000000..f22901e304
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-normal-sample-bank.osu
@@ -0,0 +1,10 @@
+osu file format v14
+
+[General]
+Mode: 3
+
+[TimingPoints]
+0,300,4,0,2,100,1,0
+
+[HitObjects]
+444,320,1000,5,1,0:0:0:0:
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-key1@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-key1@2x.png
new file mode 100644
index 0000000000..aa681f6f22
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-key1@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-stage-bottom@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-stage-bottom@2x.png
new file mode 100644
index 0000000000..ca590eaf08
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-stage-bottom@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-key1@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-key1@2x.png
new file mode 100644
index 0000000000..aa681f6f22
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-key1@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit0@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit0@2x.png
new file mode 100644
index 0000000000..2e7b9bc34f
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit0@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit100@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit100@2x.png
new file mode 100644
index 0000000000..27ca7f8b42
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit100@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit200@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit200@2x.png
new file mode 100644
index 0000000000..24ad926375
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit200@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300@2x.png
new file mode 100644
index 0000000000..098561f980
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300g-0@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300g-0@2x.png
new file mode 100644
index 0000000000..7e6501d1be
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300g-0@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300g-1@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300g-1@2x.png
new file mode 100644
index 0000000000..f17b2b1e73
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300g-1@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit50@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit50@2x.png
new file mode 100644
index 0000000000..1afec2f4a9
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit50@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-left.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-left.png
new file mode 100644
index 0000000000..03ca371c4e
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-left.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-right.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-right.png
new file mode 100644
index 0000000000..45b7be0255
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-right.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini
new file mode 100644
index 0000000000..36765d61bf
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini
@@ -0,0 +1,14 @@
+[General]
+Version: 2.5
+
+[Mania]
+Keys: 4
+ColumnLineWidth: 3,1,3,1,1
+Hit0: mania/hit0
+Hit50: mania/hit50
+Hit100: mania/hit100
+Hit200: mania/hit200
+Hit300: mania/hit300
+Hit300g: mania/hit300g
+StageLeft: mania/stage-left
+StageRight: mania/stage-right
\ No newline at end of file
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/Skinning/ColumnTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs
new file mode 100644
index 0000000000..ff4865c71d
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs
@@ -0,0 +1,40 @@
+// 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.Graphics.Containers;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.UI;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Tests.Skinning
+{
+ ///
+ /// A container to be used in a to provide a resolvable dependency.
+ ///
+ public class ColumnTestContainer : Container
+ {
+ protected override Container Content => content;
+
+ private readonly Container content;
+
+ [Cached]
+ private readonly Column column;
+
+ public ColumnTestContainer(int column, ManiaAction action)
+ {
+ this.column = new Column(column)
+ {
+ Action = { Value = action },
+ AccentColour = Color4.Orange,
+ ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd
+ };
+
+ InternalChild = content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
+ {
+ RelativeSizeAxes = Axes.Both
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
new file mode 100644
index 0000000000..18eebada00
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
@@ -0,0 +1,72 @@
+// 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.Graphics.Containers;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Tests.Skinning
+{
+ ///
+ /// A test scene for a mania hitobject.
+ ///
+ public abstract class ManiaHitObjectTestScene : ManiaSkinnableTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ SetContents(() => new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.7f,
+ Direction = FillDirection.Horizontal,
+ Children = new Drawable[]
+ {
+ new ColumnTestContainer(0, ManiaAction.Key1)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Y,
+ Width = 80,
+ Child = new ScrollingHitObjectContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ }.With(c =>
+ {
+ c.Add(CreateHitObject().With(h =>
+ {
+ h.HitObject.StartTime = START_TIME;
+ h.AccentColour.Value = Color4.Orange;
+ }));
+ })
+ },
+ new ColumnTestContainer(1, ManiaAction.Key2)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Y,
+ Width = 80,
+ Child = new ScrollingHitObjectContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ }.With(c =>
+ {
+ c.Add(CreateHitObject().With(h =>
+ {
+ h.HitObject.StartTime = START_TIME;
+ h.AccentColour.Value = Color4.Orange;
+ }));
+ })
+ },
+ }
+ });
+ }
+
+ protected abstract DrawableManiaHitObject CreateHitObject();
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs
new file mode 100644
index 0000000000..1d84a2dfcb
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs
@@ -0,0 +1,81 @@
+// 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.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Rulesets.UI.Scrolling.Algorithms;
+using osu.Game.Tests.Visual;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Tests.Skinning
+{
+ ///
+ /// A test scene for skinnable mania components.
+ ///
+ public abstract class ManiaSkinnableTestScene : SkinnableTestScene
+ {
+ protected const double START_TIME = 1000000000;
+
+ [Cached(Type = typeof(IScrollingInfo))]
+ private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo();
+
+ protected override Ruleset CreateRulesetForSkinProvider() => new ManiaRuleset();
+
+ protected ManiaSkinnableTestScene()
+ {
+ scrollingInfo.Direction.Value = ScrollingDirection.Down;
+
+ Add(new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.SlateGray.Opacity(0.2f),
+ Depth = 1
+ });
+ }
+
+ [Test]
+ public void TestScrollingDown()
+ {
+ AddStep("change direction to down", () => scrollingInfo.Direction.Value = ScrollingDirection.Down);
+ }
+
+ [Test]
+ public void TestScrollingUp()
+ {
+ AddStep("change direction to up", () => scrollingInfo.Direction.Value = ScrollingDirection.Up);
+ }
+
+ private class TestScrollingInfo : IScrollingInfo
+ {
+ public readonly Bindable Direction = new Bindable();
+
+ IBindable IScrollingInfo.Direction => Direction;
+ IBindable IScrollingInfo.TimeRange { get; } = new Bindable(1000);
+ IScrollAlgorithm IScrollingInfo.Algorithm { get; } = new ZeroScrollAlgorithm();
+ }
+
+ private class ZeroScrollAlgorithm : IScrollAlgorithm
+ {
+ public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength)
+ => double.MinValue;
+
+ public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)
+ => scrollLength;
+
+ public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
+ => (float)((time - START_TIME) / timeRange) * scrollLength;
+
+ public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
+ => 0;
+
+ public void Reset()
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs
new file mode 100644
index 0000000000..bde323f187
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.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.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Mania.UI.Components;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Tests.Skinning
+{
+ public class TestSceneColumnBackground : ManiaSkinnableTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ SetContents(() => new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.8f),
+ Direction = FillDirection.Horizontal,
+ Children = new Drawable[]
+ {
+ new ColumnTestContainer(0, ManiaAction.Key1)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Width = 0.5f,
+ Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 0), _ => new DefaultColumnBackground())
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ },
+ new ColumnTestContainer(1, ManiaAction.Key2)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Width = 0.5f,
+ Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 1), _ => new DefaultColumnBackground())
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs
new file mode 100644
index 0000000000..4392666cb7
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.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.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Mania.UI.Components;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Tests.Skinning
+{
+ public class TestSceneColumnHitObjectArea : ManiaSkinnableTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ SetContents(() => new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.8f),
+ Direction = FillDirection.Horizontal,
+ Children = new Drawable[]
+ {
+ new ColumnTestContainer(0, ManiaAction.Key1)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Width = 0.5f,
+ Child = new ColumnHitObjectArea(0, new HitObjectContainer())
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ },
+ new ColumnTestContainer(1, ManiaAction.Key2)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Width = 0.5f,
+ Child = new ColumnHitObjectArea(1, new HitObjectContainer())
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
new file mode 100644
index 0000000000..a4d4ec50f8
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
@@ -0,0 +1,36 @@
+// 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.Extensions;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Scoring;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Mania.Tests.Skinning
+{
+ public class TestSceneDrawableJudgement : ManiaSkinnableTestScene
+ {
+ public TestSceneDrawableJudgement()
+ {
+ var hitWindows = new ManiaHitWindows();
+
+ foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1))
+ {
+ if (hitWindows.IsHitResultAllowed(result))
+ {
+ AddStep("Show " + result.GetDescription(), () => SetContents(() =>
+ new DrawableManiaJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }));
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
new file mode 100644
index 0000000000..a692c0b697
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.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 System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Tests.Skinning
+{
+ [TestFixture]
+ public class TestSceneHitExplosion : ManiaSkinnableTestScene
+ {
+ public TestSceneHitExplosion()
+ {
+ int runcount = 0;
+
+ AddRepeatStep("explode", () =>
+ {
+ runcount++;
+
+ if (runcount % 15 > 12)
+ return;
+
+ CreatedDrawables.OfType().ForEach(c =>
+ {
+ c.Add(new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, 0),
+ _ => new DefaultHitExplosion((runcount / 15) % 2 == 0 ? new Color4(94, 0, 57, 255) : new Color4(6, 84, 0, 255), runcount % 6 != 0)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }));
+ });
+ }, 100);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ SetContents(() => new ColumnTestContainer(0, ManiaAction.Key1)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativePositionAxes = Axes.Y,
+ Y = -0.25f,
+ Size = new Vector2(Column.COLUMN_WIDTH, DefaultNotePiece.NOTE_HEIGHT),
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs
new file mode 100644
index 0000000000..95e86de884
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.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 System.Linq;
+using osu.Framework.Bindables;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Mania.Tests.Skinning
+{
+ public class TestSceneHoldNote : ManiaHitObjectTestScene
+ {
+ public TestSceneHoldNote()
+ {
+ AddToggleStep("toggle hitting", v =>
+ {
+ foreach (var holdNote in CreatedDrawables.SelectMany(d => d.ChildrenOfType()))
+ {
+ ((Bindable)holdNote.IsHitting).Value = v;
+ }
+ });
+ }
+
+ protected override DrawableManiaHitObject CreateHitObject()
+ {
+ var note = new HoldNote { Duration = 1000 };
+ note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ return new DrawableHoldNote(note);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs
new file mode 100644
index 0000000000..7e80419944
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.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.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Mania.UI.Components;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Tests.Skinning
+{
+ public class TestSceneKeyArea : ManiaSkinnableTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ SetContents(() => new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.8f),
+ Direction = FillDirection.Horizontal,
+ Children = new Drawable[]
+ {
+ new ColumnTestContainer(0, ManiaAction.Key1)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Width = 0.5f,
+ Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, 0), _ => new DefaultKeyArea())
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ },
+ new ColumnTestContainer(1, ManiaAction.Key2)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Width = 0.5f,
+ Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, 1), _ => new DefaultKeyArea())
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ },
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneNote.cs
new file mode 100644
index 0000000000..bc3bdf0bcb
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneNote.cs
@@ -0,0 +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 osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Mania.Tests.Skinning
+{
+ public class TestSceneNote : ManiaHitObjectTestScene
+ {
+ protected override DrawableManiaHitObject CreateHitObject()
+ {
+ var note = new Note();
+ note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ return new DrawableNote(note);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs
new file mode 100644
index 0000000000..161eda650e
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.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 System.Collections.Generic;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.UI;
+
+namespace osu.Game.Rulesets.Mania.Tests.Skinning
+{
+ public class TestScenePlayfield : ManiaSkinnableTestScene
+ {
+ private List stageDefinitions = new List();
+
+ [Test]
+ public void TestSingleStage()
+ {
+ AddStep("create stage", () =>
+ {
+ stageDefinitions = new List
+ {
+ new StageDefinition { Columns = 2 }
+ };
+
+ SetContents(() => new ManiaPlayfield(stageDefinitions));
+ });
+ }
+
+ [Test]
+ public void TestDualStages()
+ {
+ AddStep("create stage", () =>
+ {
+ stageDefinitions = new List
+ {
+ new StageDefinition { Columns = 2 },
+ new StageDefinition { Columns = 2 }
+ };
+
+ SetContents(() => new ManiaPlayfield(stageDefinitions));
+ });
+ }
+
+ protected override IBeatmap CreateBeatmapForSkinProvider()
+ {
+ var maniaBeatmap = (ManiaBeatmap)base.CreateBeatmapForSkinProvider();
+ maniaBeatmap.Stages = stageDefinitions;
+ return maniaBeatmap;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs
new file mode 100644
index 0000000000..37b97a444a
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs
@@ -0,0 +1,27 @@
+// 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.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.UI;
+
+namespace osu.Game.Rulesets.Mania.Tests.Skinning
+{
+ public class TestSceneStage : ManiaSkinnableTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ SetContents(() =>
+ {
+ ManiaAction normalAction = ManiaAction.Key1;
+ ManiaAction specialAction = ManiaAction.Special1;
+
+ return new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
+ {
+ Child = new Stage(0, new StageDefinition { Columns = 4 }, ref normalAction, ref specialAction)
+ };
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
new file mode 100644
index 0000000000..87c84cf89c
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
@@ -0,0 +1,25 @@
+// 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.Mania.UI.Components;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.Tests.Skinning
+{
+ public class TestSceneStageBackground : ManiaSkinnableTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Width = 0.5f,
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
new file mode 100644
index 0000000000..4e99068ed5
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
@@ -0,0 +1,24 @@
+// 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.Skinning;
+
+namespace osu.Game.Rulesets.Mania.Tests.Skinning
+{
+ public class TestSceneStageForeground : ManiaSkinnableTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Width = 0.5f,
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs
index d94a986dae..d9b1ad22fa 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs
@@ -12,7 +12,6 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
-using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
@@ -24,14 +23,6 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestFixture]
public class TestSceneColumn : ManiaInputTestScene
{
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(Column),
- typeof(ColumnBackground),
- typeof(ColumnKeyArea),
- typeof(ColumnHitObjectArea)
- };
-
[Cached(typeof(IReadOnlyList))]
private IReadOnlyList mods { get; set; } = Array.Empty();
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs
deleted file mode 100644
index eea1a36a19..0000000000
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs
+++ /dev/null
@@ -1,37 +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.Extensions;
-using osu.Framework.Graphics;
-using osu.Game.Rulesets.Mania.UI;
-using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.Scoring;
-
-namespace osu.Game.Rulesets.Mania.Tests
-{
- public class TestSceneDrawableJudgement : SkinnableTestScene
- {
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(DrawableJudgement),
- typeof(DrawableManiaJudgement)
- };
-
- public TestSceneDrawableJudgement()
- {
- foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1))
- {
- AddStep("Show " + result.GetDescription(), () => SetContents(() =>
- new DrawableManiaJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- }));
- }
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs
index 7ed886be49..3b9c03b86a 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs
@@ -15,8 +15,9 @@ namespace osu.Game.Rulesets.Mania.Tests
{
private readonly Bindable direction = new Bindable();
+ protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
+
public TestSceneEditor()
- : base(new ManiaRuleset())
{
AddStep("upwards scroll", () => direction.Value = ManiaScrollingDirection.Up);
AddStep("downwards scroll", () => direction.Value = ManiaScrollingDirection.Down);
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHitExplosion.cs
deleted file mode 100644
index 26a1b1b1ec..0000000000
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHitExplosion.cs
+++ /dev/null
@@ -1,62 +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.Graphics;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
-using osu.Game.Rulesets.Mania.UI;
-using osu.Game.Rulesets.UI.Scrolling;
-using osu.Game.Tests.Visual;
-using osuTK;
-using osuTK.Graphics;
-
-namespace osu.Game.Rulesets.Mania.Tests
-{
- [TestFixture]
- public class TestSceneHitExplosion : OsuTestScene
- {
- private ScrollingTestContainer scrolling;
-
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(DrawableNote),
- typeof(DrawableManiaHitObject),
- };
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- Child = scrolling = new ScrollingTestContainer(ScrollingDirection.Down)
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativePositionAxes = Axes.Y,
- Y = -0.25f,
- Size = new Vector2(Column.COLUMN_WIDTH, NotePiece.NOTE_HEIGHT),
- };
-
- int runcount = 0;
-
- AddRepeatStep("explode", () =>
- {
- runcount++;
-
- if (runcount % 15 > 12)
- return;
-
- scrolling.AddRange(new Drawable[]
- {
- new HitExplosion((runcount / 15) % 2 == 0 ? new Color4(94, 0, 57, 255) : new Color4(6, 84, 0, 255), runcount % 6 != 0)
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- }
- });
- }, 100);
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 7b0cf40d45..0d13b85901 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Mania.Tests
private const double time_after_tail = 5250;
private List judgementResults;
- private bool allJudgedFired;
///
/// -----[ ]-----
@@ -283,20 +282,15 @@ namespace osu.Game.Rulesets.Mania.Tests
{
if (currentPlayer == p) judgementResults.Add(result);
};
- p.ScoreProcessor.AllJudged += () =>
- {
- if (currentPlayer == p) allJudgedFired = true;
- };
};
LoadScreen(currentPlayer = p);
- allJudgedFired = false;
judgementResults = new List();
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
- AddUntilStep("Wait for all judged", () => allJudgedFired);
+ AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
}
private class ScoreAccessibleReplayPlayer : ReplayPlayer
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs
new file mode 100644
index 0000000000..639be0bc11
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs
@@ -0,0 +1,124 @@
+// 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 osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Edit;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Screens.Edit;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestSceneManiaBeatSnapGrid : EditorClockTestScene
+ {
+ [Cached(typeof(IScrollingInfo))]
+ private ScrollingTestContainer.TestScrollingInfo scrollingInfo = new ScrollingTestContainer.TestScrollingInfo();
+
+ [Cached(typeof(EditorBeatmap))]
+ private EditorBeatmap editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition()));
+
+ private readonly ManiaBeatSnapGrid beatSnapGrid;
+
+ public TestSceneManiaBeatSnapGrid()
+ {
+ editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 200 });
+ editorBeatmap.ControlPointInfo.Add(10000, new TimingControlPoint { BeatLength = 200 });
+
+ BeatDivisor.Value = 3;
+
+ // Some sane defaults
+ scrollingInfo.Algorithm.Algorithm = ScrollVisualisationMethod.Constant;
+ scrollingInfo.Direction.Value = ScrollingDirection.Up;
+ scrollingInfo.TimeRange.Value = 1000;
+
+ Children = new Drawable[]
+ {
+ Playfield = new ManiaPlayfield(new List
+ {
+ new StageDefinition { Columns = 4 },
+ new StageDefinition { Columns = 3 }
+ })
+ {
+ Clock = new FramedClock(new StopwatchClock())
+ },
+ new TestHitObjectComposer(Playfield)
+ {
+ Child = beatSnapGrid = new ManiaBeatSnapGrid()
+ }
+ };
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ // We're providing a constant scroll algorithm.
+ float relativePosition = Playfield.Stages[0].HitObjectContainer.ToLocalSpace(e.ScreenSpaceMousePosition).Y / Playfield.Stages[0].HitObjectContainer.DrawHeight;
+ double timeValue = scrollingInfo.TimeRange.Value * relativePosition;
+
+ beatSnapGrid.SelectionTimeRange = (timeValue, timeValue);
+
+ return true;
+ }
+
+ public ManiaPlayfield Playfield { get; }
+ }
+
+ public class TestHitObjectComposer : HitObjectComposer
+ {
+ public override Playfield Playfield { get; }
+ public override IEnumerable HitObjects => Enumerable.Empty();
+ public override bool CursorInPlacementArea => false;
+
+ public TestHitObjectComposer(Playfield playfield)
+ {
+ Playfield = playfield;
+ }
+
+ public Drawable Child
+ {
+ set => InternalChild = value;
+ }
+
+ public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override float GetBeatSnapDistanceAt(double referenceTime)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override float DurationToDistance(double referenceTime, double duration)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override double DistanceToDuration(double referenceTime, float distance)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override double GetSnappedDurationFromDistance(double referenceTime, float distance)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override float GetSnappedDistanceFromDistance(double referenceTime, float distance)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs
new file mode 100644
index 0000000000..1a3fa29d4a
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs
@@ -0,0 +1,218 @@
+// 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.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Edit;
+using osu.Game.Rulesets.Mania.Edit.Blueprints;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Screens.Edit;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestSceneManiaHitObjectComposer : EditorClockTestScene
+ {
+ private TestComposer composer;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ BeatDivisor.Value = 8;
+ Clock.Seek(0);
+
+ Child = composer = new TestComposer { RelativeSizeAxes = Axes.Both };
+ });
+
+ [Test]
+ public void TestDragOffscreenSelectionVerticallyUpScroll()
+ {
+ DrawableHitObject lastObject = null;
+ double originalTime = 0;
+ Vector2 originalPosition = Vector2.Zero;
+
+ setScrollStep(ScrollingDirection.Up);
+
+ AddStep("seek to last object", () =>
+ {
+ lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
+ originalTime = lastObject.HitObject.StartTime;
+ Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
+ });
+
+ AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
+
+ AddStep("click last object", () =>
+ {
+ originalPosition = lastObject.DrawPosition;
+
+ InputManager.MoveMouseTo(lastObject);
+ InputManager.PressButton(MouseButton.Left);
+ });
+
+ AddStep("move mouse downwards", () =>
+ {
+ InputManager.MoveMouseTo(lastObject, new Vector2(0, lastObject.ScreenSpaceDrawQuad.Height * 4));
+ InputManager.ReleaseButton(MouseButton.Left);
+ });
+
+ AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0));
+ AddAssert("hitobjects moved downwards", () => lastObject.DrawPosition.Y - originalPosition.Y > 0);
+ AddAssert("hitobject has moved time", () => lastObject.HitObject.StartTime == originalTime + 125);
+ }
+
+ [Test]
+ public void TestDragOffscreenSelectionVerticallyDownScroll()
+ {
+ DrawableHitObject lastObject = null;
+ double originalTime = 0;
+ Vector2 originalPosition = Vector2.Zero;
+
+ setScrollStep(ScrollingDirection.Down);
+
+ AddStep("seek to last object", () =>
+ {
+ lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
+ originalTime = lastObject.HitObject.StartTime;
+ Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
+ });
+
+ AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
+
+ AddStep("click last object", () =>
+ {
+ originalPosition = lastObject.DrawPosition;
+
+ InputManager.MoveMouseTo(lastObject);
+ InputManager.PressButton(MouseButton.Left);
+ });
+
+ AddStep("move mouse upwards", () =>
+ {
+ InputManager.MoveMouseTo(lastObject, new Vector2(0, -lastObject.ScreenSpaceDrawQuad.Height * 4));
+ InputManager.ReleaseButton(MouseButton.Left);
+ });
+
+ AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0));
+ AddAssert("hitobjects moved upwards", () => originalPosition.Y - lastObject.DrawPosition.Y > 0);
+ AddAssert("hitobject has moved time", () => lastObject.HitObject.StartTime == originalTime + 125);
+ }
+
+ [Test]
+ public void TestDragOffscreenSelectionHorizontally()
+ {
+ DrawableHitObject lastObject = null;
+ Vector2 originalPosition = Vector2.Zero;
+
+ setScrollStep(ScrollingDirection.Down);
+
+ AddStep("seek to last object", () =>
+ {
+ lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
+ Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
+ });
+
+ AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
+
+ AddStep("click last object", () =>
+ {
+ originalPosition = lastObject.DrawPosition;
+
+ InputManager.MoveMouseTo(lastObject);
+ InputManager.PressButton(MouseButton.Left);
+ });
+
+ AddStep("move mouse right", () =>
+ {
+ var firstColumn = composer.Composer.Playfield.GetColumn(0);
+ var secondColumn = composer.Composer.Playfield.GetColumn(1);
+
+ InputManager.MoveMouseTo(lastObject, new Vector2(secondColumn.ScreenSpaceDrawQuad.Centre.X - firstColumn.ScreenSpaceDrawQuad.Centre.X + 1, 0));
+ InputManager.ReleaseButton(MouseButton.Left);
+ });
+
+ AddAssert("hitobjects moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 1));
+
+ // Todo: They'll move vertically by the height of a note since there's no snapping and the selection point is the middle of the note.
+ AddAssert("hitobjects not moved vertically", () => lastObject.DrawPosition.Y - originalPosition.Y <= DefaultNotePiece.NOTE_HEIGHT);
+ }
+
+ [Test]
+ public void TestDragHoldNoteSelectionVertically()
+ {
+ setScrollStep(ScrollingDirection.Down);
+
+ AddStep("setup beatmap", () =>
+ {
+ composer.EditorBeatmap.Clear();
+ composer.EditorBeatmap.Add(new HoldNote
+ {
+ Column = 1,
+ EndTime = 200
+ });
+ });
+
+ DrawableHoldNote holdNote = null;
+
+ AddStep("grab hold note", () =>
+ {
+ holdNote = this.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(holdNote);
+ InputManager.PressButton(MouseButton.Left);
+ });
+
+ AddStep("move drag upwards", () =>
+ {
+ InputManager.MoveMouseTo(holdNote, new Vector2(0, -100));
+ InputManager.ReleaseButton(MouseButton.Left);
+ });
+
+ AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft));
+ AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
+
+ AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
+ AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
+ }
+
+ private void setScrollStep(ScrollingDirection direction)
+ => AddStep($"set scroll direction = {direction}", () => ((Bindable)composer.Composer.ScrollingInfo.Direction).Value = direction);
+
+ private class TestComposer : CompositeDrawable
+ {
+ [Cached(typeof(EditorBeatmap))]
+ [Cached(typeof(IBeatSnapProvider))]
+ public readonly EditorBeatmap EditorBeatmap;
+
+ public readonly ManiaHitObjectComposer Composer;
+
+ public TestComposer()
+ {
+ InternalChildren = new Drawable[]
+ {
+ EditorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 }))
+ {
+ BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }
+ },
+ Composer = new ManiaHitObjectComposer(new ManiaRuleset())
+ };
+
+ for (int i = 0; i < 10; i++)
+ EditorBeatmap.Add(new Note { StartTime = 125 * i });
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs
new file mode 100644
index 0000000000..0d726e1a50
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.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 System.Reflection;
+using NUnit.Framework;
+using osu.Framework.IO.Stores;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestSceneManiaHitObjectSamples : HitObjectSampleTest
+ {
+ protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
+ protected override IResourceStore Resources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneManiaHitObjectSamples)));
+
+ ///
+ /// Tests that when a normal sample bank is used, the normal hitsound will be looked up.
+ ///
+ [Test]
+ public void TestManiaHitObjectNormalSampleBank()
+ {
+ const string expected_sample = "normal-hitnormal2";
+
+ SetupSkins(expected_sample, expected_sample);
+
+ CreateTestWithBeatmap("mania-hitobject-beatmap-normal-sample-bank.osu");
+
+ AssertBeatmapLookup(expected_sample);
+ }
+
+ ///
+ /// Tests that when a custom sample bank is used, layered hitsounds are not played
+ /// (only the sample from the custom bank is looked up).
+ ///
+ [Test]
+ public void TestManiaHitObjectCustomSampleBank()
+ {
+ const string expected_sample = "normal-hitwhistle2";
+ const string unwanted_sample = "normal-hitnormal2";
+
+ SetupSkins(expected_sample, unwanted_sample);
+
+ CreateTestWithBeatmap("mania-hitobject-beatmap-custom-sample-bank.osu");
+
+ AssertBeatmapLookup(expected_sample);
+ AssertNoLookup(unwanted_sample);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs
similarity index 62%
rename from osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs
rename to osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs
index cd25d162d0..a399b90585 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs
@@ -5,11 +5,8 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
- public class TestScenePlayer : PlayerTestScene
+ public class TestSceneManiaPlayer : PlayerTestScene
{
- public TestScenePlayer()
- : base(new ManiaRuleset())
- {
- }
+ protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs
index d7b539a2a0..2d97e61aa5 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs
@@ -1,17 +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.Linq;
+using NUnit.Framework;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests
{
public class TestSceneNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
{
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ this.ChildrenOfType().ForEach(c => c.Clear());
+
+ ResetPlacement();
+
+ ((ScrollingTestContainer)HitObjectContainer).Direction = ScrollingDirection.Down;
+ });
+
+ [Test]
+ public void TestPlaceBeforeCurrentTimeDownwards()
+ {
+ AddStep("move mouse before current time", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single().ScreenSpaceDrawQuad.BottomLeft - new Vector2(0, 10)));
+
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("note start time < 0", () => getNote().StartTime < 0);
+ }
+
+ [Test]
+ public void TestPlaceAfterCurrentTimeDownwards()
+ {
+ AddStep("move mouse after current time", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single()));
+
+ AddStep("click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("note start time > 0", () => getNote().StartTime > 0);
+ }
+
+ private Note getNote() => this.ChildrenOfType().FirstOrDefault()?.HitObject;
+
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableNote((Note)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint();
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
index 8dae5e6d84..dd5fd93710 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
@@ -1,8 +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;
-using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -30,12 +28,6 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestFixture]
public class TestSceneNotes : OsuTestScene
{
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(DrawableNote),
- typeof(DrawableHoldNote)
- };
-
[BackgroundDependencyLoader]
private void load()
{
@@ -164,7 +156,7 @@ namespace osu.Game.Rulesets.Mania.Tests
foreach (var obj in content.OfType())
{
- if (!(obj.HitObject is IHasEndTime endTime))
+ if (!(obj.HitObject is IHasDuration endTime))
continue;
foreach (var nested in obj.NestedHitObjects)
diff --git a/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs
new file mode 100644
index 0000000000..8698ba3abd
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.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.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestScenePlayfieldCoveringContainer : OsuTestScene
+ {
+ private readonly ScrollingTestContainer scrollingContainer;
+ private readonly PlayfieldCoveringWrapper cover;
+
+ public TestScenePlayfieldCoveringContainer()
+ {
+ Child = scrollingContainer = new ScrollingTestContainer(ScrollingDirection.Down)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(300, 500),
+ Child = cover = new PlayfieldCoveringWrapper(new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Orange
+ })
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
+ };
+ }
+
+ [Test]
+ public void TestScrollingDownwards()
+ {
+ AddStep("set down scroll", () => scrollingContainer.Direction = ScrollingDirection.Down);
+ AddStep("set coverage = 0.5", () => cover.Coverage = 0.5f);
+ AddStep("set coverage = 0.8f", () => cover.Coverage = 0.8f);
+ AddStep("set coverage = 0.2f", () => cover.Coverage = 0.2f);
+ }
+
+ [Test]
+ public void TestScrollingUpwards()
+ {
+ AddStep("set up scroll", () => scrollingContainer.Direction = ScrollingDirection.Up);
+ AddStep("set coverage = 0.5", () => cover.Coverage = 0.5f);
+ AddStep("set coverage = 0.8f", () => cover.Coverage = 0.8f);
+ AddStep("set coverage = 0.2f", () => cover.Coverage = 0.2f);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
index d5fd2808b8..7376a90f17 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[Cached(typeof(IReadOnlyList))]
private IReadOnlyList mods { get; set; } = Array.Empty();
- private readonly List stages = new List();
+ private readonly List stages = new List();
private FillFlowContainer fill;
@@ -81,9 +81,9 @@ namespace osu.Game.Rulesets.Mania.Tests
AddAssert("check bar anchors", () => barsInStageAreAnchored(stages[1], Anchor.TopCentre));
}
- private bool notesInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.Columns.SelectMany(c => c.AllHitObjects).All(o => o.Anchor == anchor);
+ private bool notesInStageAreAnchored(Stage stage, Anchor anchor) => stage.Columns.SelectMany(c => c.AllHitObjects).All(o => o.Anchor == anchor);
- private bool barsInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.AllHitObjects.Where(obj => obj is DrawableBarLine).All(o => o.Anchor == anchor);
+ private bool barsInStageAreAnchored(Stage stage, Anchor anchor) => stage.AllHitObjects.Where(obj => obj is DrawableBarLine).All(o => o.Anchor == anchor);
private void createNote()
{
@@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
var specialAction = ManiaAction.Special1;
- var stage = new ManiaStage(0, new StageDefinition { Columns = 2 }, ref action, ref specialAction);
+ var stage = new Stage(0, new StageDefinition { Columns = 2 }, ref action, ref specialAction);
stages.Add(stage);
return new ScrollingTestContainer(direction)
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..972cbec4a2 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,9 +2,9 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ColumnType.cs b/osu.Game.Rulesets.Mania/Beatmaps/ColumnType.cs
new file mode 100644
index 0000000000..8f904530bc
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ColumnType.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.Mania.Beatmaps
+{
+ public enum ColumnType
+ {
+ Even,
+ Odd,
+ Special
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index 1a77a4944b..b025ac7992 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -5,7 +5,8 @@ using osu.Game.Rulesets.Mania.Objects;
using System;
using System.Linq;
using System.Collections.Generic;
-using osu.Framework.MathUtils;
+using osu.Framework.Utils;
+using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -13,7 +14,6 @@ using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
using osuTK;
-using osu.Game.Audio;
namespace osu.Game.Rulesets.Mania.Beatmaps
{
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
{
TargetColumns = (int)Math.Max(1, roundedCircleSize);
- if (TargetColumns >= 10)
+ if (TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
{
TargetColumns /= 2;
Dual = true;
@@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
}
else
{
- float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasEndTime) / beatmap.HitObjects.Count;
+ float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasDuration) / beatmap.HitObjects.Count;
if (percentSliderOrSpinner < 0.2)
TargetColumns = 7;
else if (percentSliderOrSpinner < 0.3 || roundedCircleSize >= 5)
@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
}
}
- public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition || h is ManiaHitObject);
+ public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
protected override Beatmap ConvertBeatmap(IBeatmap original)
{
@@ -176,7 +176,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
break;
}
- case IHasEndTime endTimeData:
+ case IHasDuration endTimeData:
{
conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, originalBeatmap);
@@ -232,15 +232,15 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
var pattern = new Pattern();
- if (HitObject is IHasEndTime endTimeData)
+ if (HitObject is IHasDuration endTimeData)
{
pattern.Add(new HoldNote
{
StartTime = HitObject.StartTime,
Duration = endTimeData.Duration,
Column = column,
- Head = { Samples = sampleInfoListAt(HitObject.StartTime) },
- Tail = { Samples = sampleInfoListAt(endTimeData.EndTime) },
+ Samples = HitObject.Samples,
+ NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? defaultNodeSamples
});
}
else if (HitObject is IHasXPosition)
@@ -256,21 +256,15 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
return pattern;
}
- ///
- /// Retrieves the sample info list at a point in time.
- ///
- /// The time to retrieve the sample info list from.
- ///
- private IList sampleInfoListAt(double time)
- {
- if (!(HitObject is IHasCurve curveData))
- return HitObject.Samples;
-
- double segmentTime = (curveData.EndTime - HitObject.StartTime) / curveData.SpanCount();
-
- int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime);
- return curveData.NodeSamples[index];
- }
+ ///
+ /// osu!mania-specific beatmaps in stable only play samples at the start of the hold note.
+ ///
+ private List> defaultNodeSamples
+ => new List>
+ {
+ HitObject.Samples,
+ new List()
+ };
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index 9565ac8994..d03eb0b3c9 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -4,7 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.MathUtils;
+using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
@@ -472,15 +472,22 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
/// The time to retrieve the sample info list from.
///
- private IList sampleInfoListAt(double time)
+ private IList sampleInfoListAt(double time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
+
+ ///
+ /// Retrieves the list of node samples that occur at time greater than or equal to .
+ ///
+ /// The time to retrieve node samples at.
+ private List> nodeSamplesAt(double time)
{
- if (!(HitObject is IHasCurve curveData))
- return HitObject.Samples;
+ if (!(HitObject is IHasPathWithRepeats curveData))
+ return null;
- double segmentTime = (EndTime - HitObject.StartTime) / spanCount;
+ // mathematically speaking this should be a whole number always, but floating-point arithmetic is not so kind
+ var index = (int)Math.Round(SegmentDuration == 0 ? 0 : (time - HitObject.StartTime) / SegmentDuration, MidpointRounding.AwayFromZero);
- int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime);
- return curveData.NodeSamples[index];
+ // avoid slicing the list & creating copies, if at all possible.
+ return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList();
}
///
@@ -505,16 +512,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
}
else
{
- var holdNote = new HoldNote
+ newObject = new HoldNote
{
StartTime = startTime,
- Column = column,
Duration = endTime - startTime,
- Head = { Samples = sampleInfoListAt(startTime) },
- Tail = { Samples = sampleInfoListAt(endTime) }
+ Column = column,
+ Samples = HitObject.Samples,
+ NodeSamples = nodeSamplesAt(startTime)
};
-
- newObject = holdNote;
}
pattern.Add(newObject);
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
index b3be08e1f7..d5286a3779 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, new Pattern(), originalBeatmap)
{
- endTime = (HitObject as IHasEndTime)?.EndTime ?? 0;
+ endTime = (HitObject as IHasDuration)?.EndTime ?? 0;
}
public override IEnumerable Generate()
@@ -64,21 +64,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (holdNote)
{
- var hold = new HoldNote
+ newObject = new HoldNote
{
StartTime = HitObject.StartTime,
+ Duration = endTime - HitObject.StartTime,
Column = column,
- Duration = endTime - HitObject.StartTime
+ Samples = HitObject.Samples,
+ NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
};
-
- if (hold.Head.Samples == null)
- hold.Head.Samples = new List();
-
- hold.Head.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_NORMAL });
-
- hold.Tail.Samples = HitObject.Samples;
-
- newObject = hold;
}
else
{
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs
index dff7cb72ce..2557f2acdf 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.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.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Beatmaps
@@ -21,5 +22,19 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// The 0-based column index.
/// Whether the column is a special column.
public bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2;
+
+ ///
+ /// Get the type of column given a column index.
+ ///
+ /// The 0-based column index.
+ /// The type of the column.
+ public ColumnType GetTypeOfColumn(int column)
+ {
+ if (IsSpecialColumn(column))
+ return ColumnType.Special;
+
+ int distanceToEdge = Math.Min(column, (Columns - 1) - column);
+ return distanceToEdge % 2 == 0 ? ColumnType.Odd : ColumnType.Even;
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
index f5412dcfc5..7e84f17809 100644
--- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
+++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.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.Configuration.Tracking;
using osu.Game.Configuration;
using osu.Game.Rulesets.Configuration;
@@ -19,13 +20,14 @@ namespace osu.Game.Rulesets.Mania.Configuration
{
base.InitialiseDefaults();
- Set(ManiaRulesetSetting.ScrollTime, 1500.0, 50.0, 5000.0, 50.0);
+ Set(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 1);
Set(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
}
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{
- new TrackedSetting(ManiaRulesetSetting.ScrollTime, v => new SettingDescription(v, "Scroll Time", $"{v}ms"))
+ new TrackedSetting(ManiaRulesetSetting.ScrollTime,
+ v => new SettingDescription(v, "Scroll Speed", $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / v)} ({v}ms)"))
};
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
index 3f7a2baedd..91383c5548 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
@@ -37,12 +38,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
mods = Score.Mods;
scaledScore = Score.TotalScore;
- countPerfect = Score.Statistics[HitResult.Perfect];
- countGreat = Score.Statistics[HitResult.Great];
- countGood = Score.Statistics[HitResult.Good];
- countOk = Score.Statistics[HitResult.Ok];
- countMeh = Score.Statistics[HitResult.Meh];
- countMiss = Score.Statistics[HitResult.Miss];
+ countPerfect = Score.Statistics.GetOrDefault(HitResult.Perfect);
+ countGreat = Score.Statistics.GetOrDefault(HitResult.Great);
+ countGood = Score.Statistics.GetOrDefault(HitResult.Good);
+ countOk = Score.Statistics.GetOrDefault(HitResult.Ok);
+ countMeh = Score.Statistics.GetOrDefault(HitResult.Meh);
+ countMiss = Score.Statistics.GetOrDefault(HitResult.Miss);
if (mods.Any(m => !m.Ranked))
return 0;
diff --git a/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs
new file mode 100644
index 0000000000..8d39e08b26
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs
@@ -0,0 +1,64 @@
+// 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 osu.Framework.Input.Bindings;
+
+namespace osu.Game.Rulesets.Mania
+{
+ public class DualStageVariantGenerator
+ {
+ private readonly int singleStageVariant;
+ private readonly InputKey[] stage1LeftKeys;
+ private readonly InputKey[] stage1RightKeys;
+ private readonly InputKey[] stage2LeftKeys;
+ private readonly InputKey[] stage2RightKeys;
+
+ public DualStageVariantGenerator(int singleStageVariant)
+ {
+ this.singleStageVariant = singleStageVariant;
+
+ // 10K is special because it expands towards the centre of the keyboard (VM/BN), rather than towards the edges of the keyboard.
+ if (singleStageVariant == 10)
+ {
+ stage1LeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R, InputKey.V };
+ stage1RightKeys = new[] { InputKey.M, InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft };
+
+ stage2LeftKeys = new[] { InputKey.S, InputKey.D, InputKey.F, InputKey.G, InputKey.B };
+ stage2RightKeys = new[] { InputKey.N, InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
+ }
+ else
+ {
+ stage1LeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R };
+ stage1RightKeys = new[] { InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft };
+
+ stage2LeftKeys = new[] { InputKey.S, InputKey.D, InputKey.F, InputKey.G };
+ stage2RightKeys = new[] { InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
+ }
+ }
+
+ public IEnumerable GenerateMappings()
+ {
+ var stage1Bindings = new VariantMappingGenerator
+ {
+ LeftKeys = stage1LeftKeys,
+ RightKeys = stage1RightKeys,
+ SpecialKey = InputKey.V,
+ SpecialAction = ManiaAction.Special1,
+ NormalActionStart = ManiaAction.Key1
+ }.GenerateKeyBindingsFor(singleStageVariant, out var nextNormal);
+
+ var stage2Bindings = new VariantMappingGenerator
+ {
+ LeftKeys = stage2LeftKeys,
+ RightKeys = stage2RightKeys,
+ SpecialKey = InputKey.B,
+ SpecialAction = ManiaAction.Special2,
+ NormalActionStart = nextNormal
+ }.GenerateKeyBindingsFor(singleStageVariant, out _);
+
+ return stage1Bindings.Concat(stage2Bindings);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs
index b99a1157f3..efcfe11dad 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs
@@ -7,12 +7,12 @@ using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
{
- public class EditBodyPiece : BodyPiece
+ public class EditBodyPiece : DefaultBodyPiece
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- AccentColour = colours.Yellow;
+ AccentColour.Value = colours.Yellow;
Background.Alpha = 0.5f;
Foreground.Alpha = 0;
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs
index 6f85fd9167..8773a39939 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs
@@ -12,12 +12,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
{
public EditNotePiece()
{
- Height = NotePiece.NOTE_HEIGHT;
+ Height = DefaultNotePiece.NOTE_HEIGHT;
CornerRadius = 5;
Masking = true;
- InternalChild = new NotePiece();
+ InternalChild = new DefaultNotePiece();
}
[BackgroundDependencyLoader]
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
index bcbc1ee527..b5ec1e1a2a 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
@@ -2,10 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
@@ -15,6 +20,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private readonly EditNotePiece headPiece;
private readonly EditNotePiece tailPiece;
+ [Resolved]
+ private IScrollingInfo scrollingInfo { get; set; }
+
public HoldNotePlacementBlueprint()
: base(new HoldNote())
{
@@ -34,8 +42,21 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
if (Column != null)
{
- headPiece.Y = PositionAt(HitObject.StartTime);
- tailPiece.Y = PositionAt(HitObject.EndTime);
+ headPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.StartTime)).Y;
+ tailPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.EndTime)).Y;
+
+ switch (scrollingInfo.Direction.Value)
+ {
+ case ScrollingDirection.Down:
+ headPiece.Y -= headPiece.DrawHeight / 2;
+ tailPiece.Y -= tailPiece.DrawHeight / 2;
+ break;
+
+ case ScrollingDirection.Up:
+ headPiece.Y += headPiece.DrawHeight / 2;
+ tailPiece.Y += tailPiece.DrawHeight / 2;
+ break;
+ }
}
var topPosition = new Vector2(headPiece.DrawPosition.X, Math.Min(headPiece.DrawPosition.Y, tailPiece.DrawPosition.Y));
@@ -46,25 +67,39 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
bodyPiece.Height = (bottomPosition - topPosition).Y;
}
+ protected override void OnMouseUp(MouseUpEvent e)
+ {
+ if (e.Button != MouseButton.Left)
+ return;
+
+ base.OnMouseUp(e);
+ EndPlacement(true);
+ }
+
private double originalStartTime;
- public override void UpdatePosition(Vector2 screenSpacePosition)
+ public override void UpdatePosition(SnapResult result)
{
- base.UpdatePosition(screenSpacePosition);
+ base.UpdatePosition(result);
- if (PlacementBegun)
+ if (PlacementActive)
{
- var endTime = TimeAt(screenSpacePosition);
-
- HitObject.StartTime = endTime < originalStartTime ? endTime : originalStartTime;
- HitObject.Duration = Math.Abs(endTime - originalStartTime);
+ if (result.Time is double endTime)
+ {
+ HitObject.StartTime = endTime < originalStartTime ? endTime : originalStartTime;
+ HitObject.Duration = Math.Abs(endTime - originalStartTime);
+ }
}
else
{
- headPiece.Width = tailPiece.Width = SnappedWidth;
- headPiece.X = tailPiece.X = SnappedMousePosition.X;
+ if (result.Playfield != null)
+ {
+ headPiece.Width = tailPiece.Width = result.Playfield.DrawWidth;
+ headPiece.X = tailPiece.X = ToLocalSpace(result.ScreenSpacePosition).X;
+ }
- originalStartTime = HitObject.StartTime = TimeAt(screenSpacePosition);
+ if (result.Time is double startTime)
+ originalStartTime = HitObject.StartTime = startTime;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
index 56c0b671a0..1737c4d2e5 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
@@ -4,13 +4,13 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
@@ -42,11 +42,19 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
new HoldNoteNoteSelectionBlueprint(DrawableObject, HoldNotePosition.Start),
new HoldNoteNoteSelectionBlueprint(DrawableObject, HoldNotePosition.End),
- new BodyPiece
+ new Container
{
- AccentColour = Color4.Transparent,
- BorderColour = colours.Yellow
- },
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ BorderThickness = 1,
+ BorderColour = colours.Yellow,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true,
+ }
+ }
};
}
@@ -68,5 +76,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
}
public override Quad SelectionQuad => ScreenSpaceDrawQuad;
+
+ public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre;
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
index b28d8bb0e6..27a279e044 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
@@ -1,42 +1,34 @@
// 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.Input;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Mania.UI;
-using osu.Game.Rulesets.UI.Scrolling;
-using osuTK;
+using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
- public abstract class ManiaPlacementBlueprint : PlacementBlueprint,
- IRequireHighFrequencyMousePosition // the playfield could be moving behind us
+ public abstract class ManiaPlacementBlueprint : PlacementBlueprint
where T : ManiaHitObject
{
protected new T HitObject => (T)base.HitObject;
- protected Column Column;
+ private Column column;
- ///
- /// The current mouse position, snapped to the closest column.
- ///
- protected Vector2 SnappedMousePosition { get; private set; }
+ public Column Column
+ {
+ get => column;
+ set
+ {
+ if (value == column)
+ return;
- ///
- /// The width of the closest column to the current mouse position.
- ///
- protected float SnappedWidth { get; private set; }
-
- [Resolved]
- private IManiaHitObjectComposer composer { get; set; }
-
- [Resolved]
- private IScrollingInfo scrollingInfo { get; set; }
+ column = value;
+ HitObject.Column = column.Index;
+ }
+ }
protected ManiaPlacementBlueprint(T hitObject)
: base(hitObject)
@@ -46,112 +38,22 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
protected override bool OnMouseDown(MouseDownEvent e)
{
- if (Column == null)
- return base.OnMouseDown(e);
+ if (e.Button != MouseButton.Left)
+ return false;
- HitObject.Column = Column.Index;
- BeginPlacement(TimeAt(e.ScreenSpaceMousePosition));
+ if (Column == null)
+ return false;
+
+ BeginPlacement(true);
return true;
}
- protected override bool OnMouseUp(MouseUpEvent e)
+ public override void UpdatePosition(SnapResult result)
{
- EndPlacement();
- return base.OnMouseUp(e);
- }
+ base.UpdatePosition(result);
- public override void UpdatePosition(Vector2 screenSpacePosition)
- {
- if (!PlacementBegun)
- Column = ColumnAt(screenSpacePosition);
-
- if (Column == null) return;
-
- SnappedWidth = Column.DrawWidth;
-
- // Snap to the column
- var parentPos = Parent.ToLocalSpace(Column.ToScreenSpace(new Vector2(Column.DrawWidth / 2, 0)));
- SnappedMousePosition = new Vector2(parentPos.X, Parent.ToLocalSpace(screenSpacePosition).Y);
- }
-
- protected double TimeAt(Vector2 screenSpacePosition)
- {
- if (Column == null)
- return 0;
-
- var hitObjectContainer = Column.HitObjectContainer;
-
- // If we're scrolling downwards, a position of 0 is actually further away from the hit target
- // so we need to flip the vertical coordinate in the hitobject container's space
- var hitObjectPos = mouseToHitObjectPosition(Column.HitObjectContainer.ToLocalSpace(screenSpacePosition)).Y;
- if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
- hitObjectPos = hitObjectContainer.DrawHeight - hitObjectPos;
-
- return scrollingInfo.Algorithm.TimeAt(hitObjectPos,
- EditorClock.CurrentTime,
- scrollingInfo.TimeRange.Value,
- hitObjectContainer.DrawHeight);
- }
-
- protected float PositionAt(double time)
- {
- var pos = scrollingInfo.Algorithm.PositionAt(time,
- EditorClock.CurrentTime,
- scrollingInfo.TimeRange.Value,
- Column.HitObjectContainer.DrawHeight);
-
- if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
- pos = Column.HitObjectContainer.DrawHeight - pos;
-
- return hitObjectToMousePosition(Column.HitObjectContainer.ToSpaceOfOtherDrawable(new Vector2(0, pos), Parent)).Y;
- }
-
- protected Column ColumnAt(Vector2 screenSpacePosition)
- => composer.ColumnAt(screenSpacePosition);
-
- ///
- /// Converts a mouse position to a hitobject position.
- ///
- ///
- /// Blueprints are centred on the mouse position, such that the hitobject position is anchored at the top or bottom of the blueprint depending on the scroll direction.
- ///
- /// The mouse position.
- /// The resulting hitobject position, acnhored at the top or bottom of the blueprint depending on the scroll direction.
- private Vector2 mouseToHitObjectPosition(Vector2 mousePosition)
- {
- switch (scrollingInfo.Direction.Value)
- {
- case ScrollingDirection.Up:
- mousePosition.Y -= NotePiece.NOTE_HEIGHT / 2;
- break;
-
- case ScrollingDirection.Down:
- mousePosition.Y += NotePiece.NOTE_HEIGHT / 2;
- break;
- }
-
- return mousePosition;
- }
-
- ///
- /// Converts a hitobject position to a mouse position.
- ///
- /// The hitobject position.
- /// The resulting mouse position, anchored at the centre of the hitobject.
- private Vector2 hitObjectToMousePosition(Vector2 hitObjectPosition)
- {
- switch (scrollingInfo.Direction.Value)
- {
- case ScrollingDirection.Up:
- hitObjectPosition.Y += NotePiece.NOTE_HEIGHT / 2;
- break;
-
- case ScrollingDirection.Down:
- hitObjectPosition.Y -= NotePiece.NOTE_HEIGHT / 2;
- break;
- }
-
- return hitObjectPosition;
+ if (!PlacementActive)
+ Column = result.Playfield as Column;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
index 3bd7fb2d49..384f49d9b2 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
@@ -3,8 +3,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Input.Events;
-using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
@@ -13,33 +11,19 @@ using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
- public class ManiaSelectionBlueprint : SelectionBlueprint
+ public abstract class ManiaSelectionBlueprint : OverlaySelectionBlueprint
{
- public Vector2 ScreenSpaceDragPosition { get; private set; }
- public Vector2 DragPosition { get; private set; }
-
public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject;
- protected IClock EditorClock { get; private set; }
-
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
- [Resolved]
- private IManiaHitObjectComposer composer { get; set; }
-
- public ManiaSelectionBlueprint(DrawableHitObject drawableObject)
+ protected ManiaSelectionBlueprint(DrawableHitObject drawableObject)
: base(drawableObject)
{
RelativeSizeAxes = Axes.None;
}
- [BackgroundDependencyLoader]
- private void load(IAdjustableClock clock)
- {
- EditorClock = clock;
- }
-
protected override void Update()
{
base.Update();
@@ -47,24 +31,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
Position = Parent.ToLocalSpace(DrawableObject.ToScreenSpace(Vector2.Zero));
}
- protected override bool OnMouseDown(MouseDownEvent e)
- {
- ScreenSpaceDragPosition = e.ScreenSpaceMousePosition;
- DragPosition = DrawableObject.ToLocalSpace(e.ScreenSpaceMousePosition);
-
- return base.OnMouseDown(e);
- }
-
- protected override bool OnDrag(DragEvent e)
- {
- var result = base.OnDrag(e);
-
- ScreenSpaceDragPosition = e.ScreenSpaceMousePosition;
- DragPosition = DrawableObject.ToLocalSpace(e.ScreenSpaceMousePosition);
-
- return result;
- }
-
public override void Show()
{
DrawableObject.AlwaysAlive = true;
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
index 32c6a6fd07..684004b558 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
@@ -2,29 +2,48 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
+using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public class NotePlacementBlueprint : ManiaPlacementBlueprint
{
+ private readonly EditNotePiece piece;
+
public NotePlacementBlueprint()
: base(new Note())
{
- Origin = Anchor.Centre;
+ RelativeSizeAxes = Axes.Both;
- AutoSizeAxes = Axes.Y;
-
- InternalChild = new EditNotePiece { RelativeSizeAxes = Axes.X };
+ InternalChild = piece = new EditNotePiece { Origin = Anchor.Centre };
}
- protected override void Update()
+ public override void UpdatePosition(SnapResult result)
{
- base.Update();
+ base.UpdatePosition(result);
- Width = SnappedWidth;
- Position = SnappedMousePosition;
+ if (result.Playfield != null)
+ {
+ piece.Width = result.Playfield.DrawWidth;
+ piece.Position = ToLocalSpace(result.ScreenSpacePosition);
+ }
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ if (e.Button != MouseButton.Left)
+ return false;
+
+ base.OnMouseDown(e);
+
+ // Place the note immediately.
+ EndPlacement(true);
+
+ return true;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs
deleted file mode 100644
index f64bab1fae..0000000000
--- a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs
+++ /dev/null
@@ -1,15 +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.Mania.UI;
-using osuTK;
-
-namespace osu.Game.Rulesets.Mania.Edit
-{
- public interface IManiaHitObjectComposer
- {
- Column ColumnAt(Vector2 screenSpacePosition);
-
- int TotalColumns { get; }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
new file mode 100644
index 0000000000..2028cae9a5
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
@@ -0,0 +1,215 @@
+// 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.Framework.Bindables;
+using osu.Framework.Caching;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Screens.Edit;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Edit
+{
+ ///
+ /// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor.
+ ///
+ public class ManiaBeatSnapGrid : Component
+ {
+ private const double visible_range = 750;
+
+ ///
+ /// The range of time values of the current selection.
+ ///
+ public (double start, double end)? SelectionTimeRange
+ {
+ set
+ {
+ if (value == selectionTimeRange)
+ return;
+
+ selectionTimeRange = value;
+ lineCache.Invalidate();
+ }
+ }
+
+ [Resolved]
+ private EditorBeatmap beatmap { get; set; }
+
+ [Resolved]
+ private IScrollingInfo scrollingInfo { get; set; }
+
+ [Resolved]
+ private Bindable working { get; set; }
+
+ [Resolved]
+ private OsuColour colours { get; set; }
+
+ [Resolved]
+ private BindableBeatDivisor beatDivisor { get; set; }
+
+ private readonly List grids = new List();
+
+ private readonly Cached lineCache = new Cached();
+
+ private (double start, double end)? selectionTimeRange;
+
+ [BackgroundDependencyLoader]
+ private void load(HitObjectComposer composer)
+ {
+ foreach (var stage in ((ManiaPlayfield)composer.Playfield).Stages)
+ {
+ foreach (var column in stage.Columns)
+ {
+ var lineContainer = new ScrollingHitObjectContainer();
+
+ grids.Add(lineContainer);
+ column.UnderlayElements.Add(lineContainer);
+ }
+ }
+
+ beatDivisor.BindValueChanged(_ => createLines(), true);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!lineCache.IsValid)
+ {
+ lineCache.Validate();
+ createLines();
+ }
+ }
+
+ private readonly Stack availableLines = new Stack();
+
+ private void createLines()
+ {
+ foreach (var grid in grids)
+ {
+ foreach (var line in grid.Objects.OfType())
+ availableLines.Push(line);
+
+ grid.Clear(false);
+ }
+
+ if (selectionTimeRange == null)
+ return;
+
+ var range = selectionTimeRange.Value;
+
+ var timingPoint = beatmap.ControlPointInfo.TimingPointAt(range.start - visible_range);
+
+ double time = timingPoint.Time;
+ int beat = 0;
+
+ // progress time until in the visible range.
+ while (time < range.start - visible_range)
+ {
+ time += timingPoint.BeatLength / beatDivisor.Value;
+ beat++;
+ }
+
+ while (time < range.end + visible_range)
+ {
+ var nextTimingPoint = beatmap.ControlPointInfo.TimingPointAt(time);
+
+ // switch to the next timing point if we have reached it.
+ if (nextTimingPoint.Time > timingPoint.Time)
+ {
+ beat = 0;
+ time = nextTimingPoint.Time;
+ timingPoint = nextTimingPoint;
+ }
+
+ Color4 colour = BindableBeatDivisor.GetColourFor(
+ BindableBeatDivisor.GetDivisorForBeatIndex(Math.Max(1, beat), beatDivisor.Value), colours);
+
+ foreach (var grid in grids)
+ {
+ if (!availableLines.TryPop(out var line))
+ line = new DrawableGridLine();
+
+ line.HitObject.StartTime = time;
+ line.Colour = colour;
+
+ grid.Add(line);
+ }
+
+ beat++;
+ time += timingPoint.BeatLength / beatDivisor.Value;
+ }
+
+ foreach (var grid in grids)
+ {
+ // required to update ScrollingHitObjectContainer's cache.
+ grid.UpdateSubTree();
+
+ foreach (var line in grid.Objects.OfType())
+ {
+ time = line.HitObject.StartTime;
+
+ if (time >= range.start && time <= range.end)
+ line.Alpha = 1;
+ else
+ {
+ double timeSeparation = time < range.start ? range.start - time : time - range.end;
+ line.Alpha = (float)Math.Max(0, 1 - timeSeparation / visible_range);
+ }
+ }
+ }
+ }
+
+ private class DrawableGridLine : DrawableHitObject
+ {
+ [Resolved]
+ private IScrollingInfo scrollingInfo { get; set; }
+
+ private readonly IBindable direction = new Bindable();
+
+ public DrawableGridLine()
+ : base(new HitObject())
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = 2;
+
+ AddInternal(new Box { RelativeSizeAxes = Axes.Both });
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ Origin = Anchor = direction.NewValue == ScrollingDirection.Up
+ ? Anchor.TopLeft
+ : Anchor.BottomLeft;
+ }
+
+ protected override void UpdateInitialTransforms()
+ {
+ // don't perform any fading – we are handling that ourselves.
+ }
+
+ protected override void UpdateStateTransforms(ArmedState state)
+ {
+ LifetimeEnd = HitObject.StartTime + visible_range;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
new file mode 100644
index 0000000000..cea27498c3
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
@@ -0,0 +1,36 @@
+// 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.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Mania.Edit.Blueprints;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Screens.Edit.Compose.Components;
+
+namespace osu.Game.Rulesets.Mania.Edit
+{
+ public class ManiaBlueprintContainer : ComposeBlueprintContainer
+ {
+ public ManiaBlueprintContainer(IEnumerable drawableHitObjects)
+ : base(drawableHitObjects)
+ {
+ }
+
+ public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case DrawableNote note:
+ return new NoteSelectionBlueprint(note);
+
+ case DrawableHoldNote holdNote:
+ return new HoldNoteSelectionBlueprint(holdNote);
+ }
+
+ return base.CreateBlueprintFor(hitObject);
+ }
+
+ protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler();
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
index 1632b6a583..7e2469a794 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
@@ -5,42 +5,79 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.Objects.Drawables;
using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
-using osu.Game.Rulesets.Mania.Edit.Blueprints;
+using osu.Framework.Input;
+using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Mania.Edit
{
- [Cached(Type = typeof(IManiaHitObjectComposer))]
- public class ManiaHitObjectComposer : HitObjectComposer, IManiaHitObjectComposer
+ public class ManiaHitObjectComposer : HitObjectComposer
{
private DrawableManiaEditRuleset drawableRuleset;
+ private ManiaBeatSnapGrid beatSnapGrid;
+ private InputManager inputManager;
public ManiaHitObjectComposer(Ruleset ruleset)
: base(ruleset)
{
}
- ///
- /// Retrieves the column that intersects a screen-space position.
- ///
- /// The screen-space position.
- /// The column which intersects with .
- public Column ColumnAt(Vector2 screenSpacePosition) => drawableRuleset.GetColumnByPosition(screenSpacePosition);
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddInternal(beatSnapGrid = new ManiaBeatSnapGrid());
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ inputManager = GetContainingInputManager();
+ }
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
- public int TotalColumns => ((ManiaPlayfield)drawableRuleset.Playfield).TotalColumns;
+ public new ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield);
+
+ public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo;
+
+ protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
+ Playfield.GetColumnByPosition(screenSpacePosition);
+
+ public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
+ {
+ var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
+
+ switch (ScrollingInfo.Direction.Value)
+ {
+ case ScrollingDirection.Down:
+ result.ScreenSpacePosition -= new Vector2(0, getNoteHeight() / 2);
+ break;
+
+ case ScrollingDirection.Up:
+ result.ScreenSpacePosition += new Vector2(0, getNoteHeight() / 2);
+ break;
+ }
+
+ return result;
+ }
+
+ private float getNoteHeight() =>
+ Playfield.GetColumn(0).ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
+ Playfield.GetColumn(0).ToScreenSpace(Vector2.Zero).Y;
protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
{
@@ -52,26 +89,36 @@ namespace osu.Game.Rulesets.Mania.Edit
return drawableRuleset;
}
+ protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects)
+ => new ManiaBlueprintContainer(hitObjects);
+
protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[]
{
new NoteCompositionTool(),
new HoldNoteCompositionTool()
};
- public override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler();
-
- public override SelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject)
+ protected override void UpdateAfterChildren()
{
- switch (hitObject)
+ base.UpdateAfterChildren();
+
+ if (BlueprintContainer.CurrentTool is SelectTool)
{
- case DrawableNote note:
- return new NoteSelectionBlueprint(note);
-
- case DrawableHoldNote holdNote:
- return new HoldNoteSelectionBlueprint(holdNote);
+ if (EditorBeatmap.SelectedHitObjects.Any())
+ {
+ beatSnapGrid.SelectionTimeRange = (EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime()));
+ }
+ else
+ beatSnapGrid.SelectionTimeRange = null;
+ }
+ else
+ {
+ var result = SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position);
+ if (result.Time is double time)
+ beatSnapGrid.SelectionTimeRange = (time, time);
+ else
+ beatSnapGrid.SelectionTimeRange = null;
}
-
- return base.CreateBlueprintFor(hitObject);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index 618af3e772..65f40d7d0a 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
@@ -4,10 +4,9 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Timing;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
@@ -19,88 +18,23 @@ namespace osu.Game.Rulesets.Mania.Edit
private IScrollingInfo scrollingInfo { get; set; }
[Resolved]
- private IManiaHitObjectComposer composer { get; set; }
-
- private IClock editorClock;
-
- [BackgroundDependencyLoader]
- private void load(IAdjustableClock clock)
- {
- editorClock = clock;
- }
+ private HitObjectComposer composer { get; set; }
public override bool HandleMovement(MoveSelectionEvent moveEvent)
{
var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint;
int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column;
- adjustOrigins(maniaBlueprint);
- performDragMovement(moveEvent);
performColumnMovement(lastColumn, moveEvent);
return true;
}
- ///
- /// Ensures that the position of hitobjects remains centred to the mouse position.
- /// E.g. The hitobject position will change if the editor scrolls while a hitobject is dragged.
- ///
- /// The that received the drag event.
- private void adjustOrigins(ManiaSelectionBlueprint reference)
- {
- var referenceParent = (HitObjectContainer)reference.DrawableObject.Parent;
-
- float offsetFromReferenceOrigin = reference.DragPosition.Y - reference.DrawableObject.OriginPosition.Y;
- float targetPosition = referenceParent.ToLocalSpace(reference.ScreenSpaceDragPosition).Y - offsetFromReferenceOrigin;
-
- // Flip the vertical coordinate space when scrolling downwards
- if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
- targetPosition -= referenceParent.DrawHeight;
-
- float movementDelta = targetPosition - reference.DrawableObject.Position.Y;
-
- foreach (var b in SelectedBlueprints.OfType())
- b.DrawableObject.Y += movementDelta;
- }
-
- private void performDragMovement(MoveSelectionEvent moveEvent)
- {
- float delta = moveEvent.InstantDelta.Y;
-
- // When scrolling downwards the anchor position is at the bottom of the screen, however the movement event assumes the anchor is at the top of the screen.
- // This causes the delta to assume a positive hitobject position, and which can be corrected for by subtracting the parent height.
- if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
- delta -= moveEvent.Blueprint.DrawableObject.Parent.DrawHeight;
-
- foreach (var b in SelectedBlueprints)
- {
- var hitObject = b.DrawableObject;
- var objectParent = (HitObjectContainer)hitObject.Parent;
-
- // StartTime could be used to adjust the position if only one movement event was received per frame.
- // However this is not the case and ScrollingHitObjectContainer performs movement in UpdateAfterChildren() so the position must also be updated to be valid for further movement events
- hitObject.Y += delta;
-
- float targetPosition = hitObject.Position.Y;
-
- // The scrolling algorithm always assumes an anchor at the top of the screen, so the position must be flipped when scrolling downwards to reflect a top anchor
- if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
- targetPosition = -targetPosition;
-
- objectParent.Remove(hitObject);
-
- hitObject.HitObject.StartTime = scrollingInfo.Algorithm.TimeAt(targetPosition,
- editorClock.CurrentTime,
- scrollingInfo.TimeRange.Value,
- objectParent.DrawHeight);
-
- objectParent.Add(hitObject);
- }
- }
-
private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent)
{
- var currentColumn = composer.ColumnAt(moveEvent.ScreenSpacePosition);
+ var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield;
+
+ var currentColumn = maniaPlayfield.GetColumnByPosition(moveEvent.ScreenSpacePosition);
if (currentColumn == null)
return;
@@ -119,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.Edit
maxColumn = obj.Column;
}
- columnDelta = Math.Clamp(columnDelta, -minColumn, composer.TotalColumns - 1 - maxColumn);
+ columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn);
foreach (var obj in SelectedHitObjects.OfType())
obj.Column += columnDelta;
diff --git a/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs
deleted file mode 100644
index ff8882124f..0000000000
--- a/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs
+++ /dev/null
@@ -1,18 +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.Graphics;
-using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Objects.Drawables;
-
-namespace osu.Game.Rulesets.Mania.Edit.Masks
-{
- public abstract class ManiaSelectionBlueprint : SelectionBlueprint
- {
- protected ManiaSelectionBlueprint(DrawableHitObject drawableObject)
- : base(drawableObject)
- {
- RelativeSizeAxes = Axes.None;
- }
- }
-}
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/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
index b9c6e3a7f7..294aab1e4e 100644
--- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
+++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
@@ -7,19 +7,17 @@ namespace osu.Game.Rulesets.Mania.Judgements
{
public class HoldNoteTickJudgement : ManiaJudgement
{
- public override bool AffectsCombo => false;
-
protected override int NumericResultFor(HitResult result) => 20;
protected override double HealthIncreaseFor(HitResult result)
{
switch (result)
{
- case HitResult.Miss:
+ default:
return 0;
- default:
- return 0.040;
+ case HitResult.Perfect:
+ return 0.01;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
index 0e4c811945..53db676a54 100644
--- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
@@ -25,35 +25,10 @@ namespace osu.Game.Rulesets.Mania.Judgements
return 200;
case HitResult.Great:
- case HitResult.Perfect:
return 300;
- }
- }
-
- protected override double HealthIncreaseFor(HitResult result)
- {
- switch (result)
- {
- case HitResult.Miss:
- return -0.125;
-
- case HitResult.Meh:
- return 0.005;
-
- case HitResult.Ok:
- return 0.010;
-
- case HitResult.Good:
- return 0.035;
-
- case HitResult.Great:
- return 0.055;
case HitResult.Perfect:
- return 0.065;
-
- default:
- return 0;
+ return 320;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/ManiaInputManager.cs b/osu.Game.Rulesets.Mania/ManiaInputManager.cs
index 292990fd7e..186fc4b15d 100644
--- a/osu.Game.Rulesets.Mania/ManiaInputManager.cs
+++ b/osu.Game.Rulesets.Mania/ManiaInputManager.cs
@@ -78,5 +78,11 @@ namespace osu.Game.Rulesets.Mania
[Description("Key 18")]
Key18,
+
+ [Description("Key 19")]
+ Key19,
+
+ [Description("Key 20")]
+ Key20,
}
}
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index c50f4314a3..68dce8b139 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -12,6 +12,7 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
+using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types;
@@ -30,15 +31,24 @@ using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osu.Game.Scoring;
+using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Mania
{
+ [ExcludeFromDynamicCompile]
public class ManiaRuleset : Ruleset, ILegacyRuleset
{
+ ///
+ /// The maximum number of supported keys in a single stage.
+ ///
+ public const int MAX_STAGE_KEYS = 10;
+
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableManiaRuleset(this, beatmap, mods);
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
+ public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new DrainingHealthProcessor(drainStartTime, 0.2);
+
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new ManiaPerformanceCalculator(this, beatmap, score);
@@ -47,9 +57,9 @@ namespace osu.Game.Rulesets.Mania
public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this);
- public override ISkin CreateLegacySkinProvider(ISkinSource source) => new ManiaLegacySkinTransformer(source);
+ public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new ManiaLegacySkinTransformer(source, beatmap);
- public override IEnumerable ConvertLegacyMods(LegacyMods mods)
+ public override IEnumerable ConvertFromLegacyMods(LegacyMods mods)
{
if (mods.HasFlag(LegacyMods.Nightcore))
yield return new ManiaModNightcore();
@@ -118,6 +128,59 @@ namespace osu.Game.Rulesets.Mania
yield return new ManiaModRandom();
}
+ public override LegacyMods ConvertToLegacyMods(Mod[] mods)
+ {
+ var value = base.ConvertToLegacyMods(mods);
+
+ foreach (var mod in mods)
+ {
+ switch (mod)
+ {
+ case ManiaModKey1 _:
+ value |= LegacyMods.Key1;
+ break;
+
+ case ManiaModKey2 _:
+ value |= LegacyMods.Key2;
+ break;
+
+ case ManiaModKey3 _:
+ value |= LegacyMods.Key3;
+ break;
+
+ case ManiaModKey4 _:
+ value |= LegacyMods.Key4;
+ break;
+
+ case ManiaModKey5 _:
+ value |= LegacyMods.Key5;
+ break;
+
+ case ManiaModKey6 _:
+ value |= LegacyMods.Key6;
+ break;
+
+ case ManiaModKey7 _:
+ value |= LegacyMods.Key7;
+ break;
+
+ case ManiaModKey8 _:
+ value |= LegacyMods.Key8;
+ break;
+
+ case ManiaModKey9 _:
+ value |= LegacyMods.Key9;
+ break;
+
+ case ManiaModFadeIn _:
+ value |= LegacyMods.FadeIn;
+ break;
+ }
+ }
+
+ return value;
+ }
+
public override IEnumerable GetModsFor(ModType type)
{
switch (type)
@@ -149,6 +212,7 @@ namespace osu.Game.Rulesets.Mania
new ManiaModKey7(),
new ManiaModKey8(),
new ManiaModKey9(),
+ new ManiaModKey10(),
new ManiaModKey1(),
new ManiaModKey2(),
new ManiaModKey3()),
@@ -179,6 +243,8 @@ namespace osu.Game.Rulesets.Mania
public override string ShortName => SHORT_NAME;
+ public override string PlayingVerb => "Smashing keys";
+
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetMania };
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(this, beatmap);
@@ -195,9 +261,9 @@ namespace osu.Game.Rulesets.Mania
{
get
{
- for (int i = 1; i <= 9; i++)
+ for (int i = 1; i <= MAX_STAGE_KEYS; i++)
yield return (int)PlayfieldType.Single + i;
- for (int i = 2; i <= 18; i += 2)
+ for (int i = 2; i <= MAX_STAGE_KEYS * 2; i += 2)
yield return (int)PlayfieldType.Dual + i;
}
}
@@ -207,73 +273,10 @@ namespace osu.Game.Rulesets.Mania
switch (getPlayfieldType(variant))
{
case PlayfieldType.Single:
- return new VariantMappingGenerator
- {
- LeftKeys = new[]
- {
- InputKey.A,
- InputKey.S,
- InputKey.D,
- InputKey.F
- },
- RightKeys = new[]
- {
- InputKey.J,
- InputKey.K,
- InputKey.L,
- InputKey.Semicolon
- },
- SpecialKey = InputKey.Space,
- SpecialAction = ManiaAction.Special1,
- NormalActionStart = ManiaAction.Key1,
- }.GenerateKeyBindingsFor(variant, out _);
+ return new SingleStageVariantGenerator(variant).GenerateMappings();
case PlayfieldType.Dual:
- int keys = getDualStageKeyCount(variant);
-
- var stage1Bindings = new VariantMappingGenerator
- {
- LeftKeys = new[]
- {
- InputKey.Number1,
- InputKey.Number2,
- InputKey.Number3,
- InputKey.Number4,
- },
- RightKeys = new[]
- {
- InputKey.Z,
- InputKey.X,
- InputKey.C,
- InputKey.V
- },
- SpecialKey = InputKey.Tilde,
- SpecialAction = ManiaAction.Special1,
- NormalActionStart = ManiaAction.Key1
- }.GenerateKeyBindingsFor(keys, out var nextNormal);
-
- var stage2Bindings = new VariantMappingGenerator
- {
- LeftKeys = new[]
- {
- InputKey.Number7,
- InputKey.Number8,
- InputKey.Number9,
- InputKey.Number0
- },
- RightKeys = new[]
- {
- InputKey.O,
- InputKey.P,
- InputKey.BracketLeft,
- InputKey.BracketRight
- },
- SpecialKey = InputKey.BackSlash,
- SpecialAction = ManiaAction.Special2,
- NormalActionStart = nextNormal
- }.GenerateKeyBindingsFor(keys, out _);
-
- return stage1Bindings.Concat(stage2Bindings);
+ return new DualStageVariantGenerator(getDualStageKeyCount(variant)).GenerateMappings();
}
return Array.Empty();
@@ -310,58 +313,20 @@ namespace osu.Game.Rulesets.Mania
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast