diff --git a/.github/ISSUE_TEMPLATE/01-bug-issues.md b/.github/ISSUE_TEMPLATE/01-bug-issues.md
index 0aff276d03..0b80ce44dd 100644
--- a/.github/ISSUE_TEMPLATE/01-bug-issues.md
+++ b/.github/ISSUE_TEMPLATE/01-bug-issues.md
@@ -9,6 +9,8 @@ about: Issues regarding encountered bugs.
**osu!lazer version:**
**Logs:**
+
diff --git a/.github/ISSUE_TEMPLATE/02-crash-issues.md b/.github/ISSUE_TEMPLATE/02-crash-issues.md
index 9c3ae33161..ada8de73c0 100644
--- a/.github/ISSUE_TEMPLATE/02-crash-issues.md
+++ b/.github/ISSUE_TEMPLATE/02-crash-issues.md
@@ -9,8 +9,10 @@ about: Issues regarding crashes or permanent freezes.
**osu!lazer version:**
**Logs:**
+
**Computer Specifications:**
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/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
index d85a0ae44c..31f1fda09d 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml
@@ -1,8 +1,8 @@
-
+
-
+
@@ -12,9 +12,9 @@
-
+
-
+
\ 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 6480612b2e..4e8af405a2 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -11,11 +11,6 @@
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Debug)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
- }
- },
"console": "internalConsole"
},
{
@@ -28,11 +23,6 @@
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Release)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
- }
- },
"console": "internalConsole"
},
{
@@ -45,11 +35,6 @@
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build tests (Debug)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
- }
- },
"console": "internalConsole"
},
{
@@ -62,11 +47,6 @@
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build tests (Release)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
- }
- },
"console": "internalConsole"
},
{
@@ -80,11 +60,6 @@
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Debug)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
- }
- },
"console": "internalConsole"
},
{
@@ -98,11 +73,6 @@
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Release)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
- }
- },
"console": "internalConsole"
},
{
@@ -116,11 +86,6 @@
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build tournament tests (Debug)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
- }
- },
"console": "internalConsole"
},
{
@@ -134,11 +99,6 @@
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build tournament tests (Release)",
- "linux": {
- "env": {
- "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}"
- }
- },
"console": "internalConsole"
},
{
@@ -169,4 +129,4 @@
"externalConsole": false
}
]
-}
\ No newline at end of file
+}
diff --git a/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 21b8b402e0..2cd40c8675 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -16,9 +16,9 @@
-
+
-
+
$(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset
diff --git a/Gemfile.lock b/Gemfile.lock
index e3954c2681..bf971d2c22 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -5,6 +5,22 @@ GEM
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,23 +29,24 @@ 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.71.1)
- faraday (0.17.3)
+ 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.140.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
@@ -37,12 +54,12 @@ GEM
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
excon (>= 0.71.0, < 1.0.0)
- faraday (~> 0.17)
+ faraday (>= 0.17, < 2.0)
faraday-cookie_jar (~> 0.0.6)
- faraday_middleware (~> 0.13.1)
+ 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.29.2, < 0.37.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)
@@ -69,7 +86,7 @@ GEM
souyuz (= 0.9.1)
fastlane-plugin-xamarin (0.6.3)
gh_inspector (1.1.3)
- google-api-client (0.36.4)
+ google-api-client (0.38.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
@@ -80,27 +97,28 @@ GEM
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
- google-cloud-env (1.3.0)
- faraday (~> 0.11)
- google-cloud-errors (1.0.0)
- google-cloud-storage (1.25.1)
+ 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.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
- googleauth (0.10.0)
- faraday (~> 0.12)
+ 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.12)
+ signet (~> 0.14)
highline (1.7.10)
http-cookie (1.0.3)
domain_name (~> 0.5)
httpclient (2.8.3)
+ jmespath (1.4.0)
json (2.3.0)
jwt (2.1.0)
memoist (0.16.2)
@@ -114,7 +132,7 @@ GEM
naturally (2.2.0)
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)
@@ -125,12 +143,12 @@ 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.7)
+ simctl (1.6.8)
CFPropertyList
naturally
slack-notifier (2.3.2)
@@ -141,17 +159,17 @@ GEM
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.2)
+ 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.1)
+ unf_ext (0.0.7.7)
+ unicode-display_width (1.7.0)
word_wrap (1.0.0)
- xcodeproj (1.14.0)
+ xcodeproj (1.16.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
diff --git a/README.md b/README.md
index ae57b1d954..dc3ee63844 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,8 @@ Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the
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.
+**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.
+
We are accepting bug reports (please report with as much detail as possible and follow the existing issue templates). Feature requests are also welcome, but understand that our focus is on completing the game to feature parity before adding new features. A few resources are available as starting points to getting involved and understanding the project:
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
@@ -27,7 +29,7 @@ If you are looking to install or test osu! without setting up a development envi
**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) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.x86_64.AppImage) | [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)
| ------------- | ------------- | ------------- | ------------- | ------------- |
- 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.
@@ -91,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 2e7a1d1b28..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.2"
+#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 510b53054b..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
diff --git a/global.json b/global.json
index 6858d4044d..9aa5b6192b 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.50"
}
}
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index 28fbdb3367..0881861bdc 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,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.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 bd91bcc933..285a813d97 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -33,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 c34e1e1221..7a99c70999 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -30,6 +30,10 @@
+
+
+
+
diff --git a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs
index 394fd75488..1d207d04c7 100644
--- a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs
+++ b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs
@@ -8,7 +8,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.IO.Archives;
-using osu.Game.Resources;
+using osu.Game.Tests.Resources;
namespace osu.Game.Benchmarks
{
@@ -18,8 +18,8 @@ namespace osu.Game.Benchmarks
public override void SetUp()
{
- using (var resources = new DllResourceStore(OsuResources.ResourceAssembly))
- using (var archive = resources.GetStream("Beatmaps/241526 Soleily - Renatus.osz"))
+ 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);
}
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index f2e1c0ec3b..41e726e05c 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -7,12 +7,13 @@
-
+
+
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
index f4749be370..df54df7b01 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
@@ -8,7 +8,6 @@ using NUnit.Framework;
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/special-skin/fruit-catcher-fail.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail@2x.png
similarity index 100%
rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png
rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail@2x.png
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-idle.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle.png
deleted file mode 100755
index 17177f3246..0000000000
Binary files a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle.png and /dev/null differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai@2x.png
similarity index 100%
rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png
rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai@2x.png
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 20911b8d06..e89a95ae37 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
@@ -1,34 +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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.Objects.Drawables;
-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()
{
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 4ff9f7a7fe..6eeda2c731 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -4,25 +4,18 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Catch.UI;
-using osu.Game.Tests.Visual;
-using System;
-using System.Collections.Generic;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneCatcher : SkinnableTestScene
+ public class TestSceneCatcher : CatchSkinnableTestScene
{
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(CatcherArea),
- };
-
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new CatcherArea.Catcher
+ SetContents(() => new Catcher(new Container())
{
RelativePositionAxes = Axes.None,
Anchor = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index df1ac4c725..fbb22a8498 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.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;
@@ -10,23 +8,23 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.Beatmaps;
+using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
-using osu.Game.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 : SkinnableTestScene
+ public class TestSceneCatcherArea : CatchSkinnableTestScene
{
private RulesetInfo catchRuleset;
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(CatcherArea),
- };
-
public TestSceneCatcherArea()
{
AddSliderStep("CircleSize", 0, 8, 5, createCatcher);
@@ -34,9 +32,41 @@ namespace osu.Game.Rulesets.Catch.Tests
CreatedDrawables.OfType().Select(i => i.Child)
.OfType().ForEach(c => c.ToggleHyperDash(t)));
- AddRepeatStep("catch fruit", () =>
- this.ChildrenOfType().ForEach(area =>
- area.MovableCatcher.PlaceOnPlate(new DrawableFruit(new TestSceneFruitObjects.TestCatchFruit(FruitVisualRepresentation.Grape)))), 20);
+ AddRepeatStep("catch fruit", () => catchFruit(new TestFruit(false)
+ {
+ X = this.ChildrenOfType().First().MovableCatcher.X
+ }), 20);
+ AddRepeatStep("catch fruit last in combo", () => catchFruit(new TestFruit(false)
+ {
+ X = this.ChildrenOfType().First().MovableCatcher.X,
+ LastInCombo = true,
+ }), 20);
+ AddRepeatStep("catch kiai fruit", () => catchFruit(new TestFruit(true)
+ {
+ X = this.ChildrenOfType().First().MovableCatcher.X,
+ }), 20);
+ AddRepeatStep("miss fruit", () => catchFruit(new Fruit
+ {
+ X = this.ChildrenOfType().First().MovableCatcher.X + 100,
+ LastInCombo = true,
+ }, true), 20);
+ }
+
+ private void catchFruit(Fruit fruit, bool miss = false)
+ {
+ this.ChildrenOfType().ForEach(area =>
+ {
+ DrawableFruit drawable = new DrawableFruit(fruit);
+ area.Add(drawable);
+
+ Schedule(() =>
+ {
+ area.AttemptCatch(fruit);
+ area.OnResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great });
+
+ drawable.Expire();
+ });
+ });
}
private void createCatcher(float size)
@@ -46,8 +76,9 @@ namespace osu.Game.Rulesets.Catch.Tests
RelativeSizeAxes = Axes.Both,
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
},
});
}
@@ -58,6 +89,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)
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
index 070847c0c1..d35f828e28 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
@@ -1,10 +1,9 @@
// 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;
@@ -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 82d5aa936f..c07e4fdad3 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
@@ -2,36 +2,17 @@
// 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.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
-using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
-using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneFruitObjects : SkinnableTestScene
+ public class TestSceneFruitObjects : CatchSkinnableTestScene
{
- public override IReadOnlyList RequiredTypes => new[]
- {
- typeof(CatchHitObject),
- typeof(Fruit),
- typeof(FruitPiece),
- typeof(Droplet),
- typeof(Banana),
- typeof(BananaShower),
- typeof(DrawableCatchHitObject),
- typeof(DrawableFruit),
- typeof(DrawableDroplet),
- typeof(DrawableBanana),
- typeof(DrawableBananaShower),
- typeof(Pulp),
- };
-
protected override void LoadComplete()
{
base.LoadComplete();
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
index cbc87459e1..269e783899 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs
@@ -5,20 +5,15 @@ 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 osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
- public class TestSceneJuiceStream : PlayerTestScene
+ public class TestSceneJuiceStream : TestSceneCatchPlayer
{
- public TestSceneJuiceStream()
- : base(new CatchRuleset())
- {
- }
-
[Test]
public void TestJuiceStreamEndingCombo()
{
@@ -36,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
new JuiceStream
{
- X = 0.5f,
+ X = CatchPlayfield.CENTER_X,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
@@ -46,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Tests
},
new Banana
{
- X = 0.5f,
+ 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 8c371db257..cbd3dc5518 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/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 1a5d0f983b..bb14988414 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -28,8 +28,6 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
ApplyPositionOffsets(Beatmap);
- initialiseHyperDash((List)Beatmap.HitObjects);
-
int index = 0;
foreach (var obj in Beatmap.HitObjects.OfType())
@@ -67,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
@@ -76,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
}
@@ -90,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;
@@ -116,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)
{
@@ -132,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;
@@ -150,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;
@@ -191,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)
{
@@ -212,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;
@@ -221,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 b9d791fdb1..9437023c70 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -21,11 +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);
@@ -48,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();
@@ -143,7 +145,7 @@ namespace osu.Game.Rulesets.Catch
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
- public override ISkin CreateLegacySkinProvider(ISkinSource source) => new CatchLegacySkinTransformer(source);
+ public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new CatchPerformanceCalculator(this, beatmap, score);
diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
index 02c045f363..80390705fe 100644
--- a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
+++ b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
@@ -10,6 +10,9 @@ namespace osu.Game.Rulesets.Catch
FruitGrapes,
FruitOrange,
FruitPear,
- Droplet
+ 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 fc030877f1..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.01;
+ return DEFAULT_MAX_HEALTH_INCREASE * 0.75;
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
index e2465d727e..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;
@@ -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/CatchModPerfect.cs b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs
index fb92399102..e3391c47f1 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs
@@ -1,11 +1,17 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Catch.Judgements;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModPerfect : ModPerfect
{
+ protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
+ => !(result.Judgement is CatchBananaJudgement)
+ && base.FailCondition(healthProcessor, result);
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
index 4c72b9fd3e..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,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Mods
RelativeSizeAxes = Axes.Both;
}
- //disable keyboard controls
+ // disable keyboard controls
public bool OnPressed(CatchAction action) => true;
public void OnReleased(CatchAction action)
diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
index c3488aec11..04a995c77e 100644
--- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
@@ -1,12 +1,13 @@
// 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;
@@ -14,13 +15,13 @@ namespace osu.Game.Rulesets.Catch.Objects
public override Judgement CreateJudgement() => new IgnoreJudgement();
- protected override void CreateNestedHitObjects()
+ 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)
@@ -31,6 +32,8 @@ namespace osu.Game.Rulesets.Catch.Objects
for (double i = StartTime; i <= EndTime; i += spacing)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
AddNested(new Banana
{
Samples = Samples,
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index f3b566f340..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;
@@ -17,6 +18,9 @@ namespace osu.Game.Rulesets.Catch.Objects
private float x;
+ ///
+ /// The horizontal position of the fruit between 0 and .
+ ///
public float X
{
get => x + XOffset;
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
index cf7231ebb2..01b76ceed9 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
@@ -2,6 +2,7 @@
// 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;
@@ -22,6 +23,23 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
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))
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
index 5bfe0515a1..c6345a9df7 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
@@ -9,6 +9,7 @@ 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;
@@ -70,10 +71,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
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;
}
@@ -91,10 +93,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
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
index 0a8e830af9..cad8892283 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
float startRotation = RNG.NextSingle() * 20;
double duration = HitObject.TimePreempt + 2000;
- this.RotateTo(startRotation).RotateTo(startRotation + 720, duration);
+ 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
index 197ad41247..fae5a10d04 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
@@ -13,7 +13,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public DrawableFruit(Fruit h)
: base(h)
{
- Rotation = (float)(RNG.NextDouble() - 0.5f) * 40;
}
[BackgroundDependencyLoader]
@@ -21,6 +20,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
ScaleContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(getComponent(HitObject.VisualRepresentation)), _ => new FruitPiece());
+
+ ScaleContainer.Rotation = (float)(RNG.NextDouble() - 0.5f) * 40;
}
private CatchSkinComponents getComponent(FruitVisualRepresentation hitObjectVisualRepresentation)
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
index 932464cfd1..7bc016d94f 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
@@ -6,6 +6,7 @@ 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.Drawables
{
@@ -14,11 +15,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
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.Drawables
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
+ hitObject.Origin = Anchor.BottomCentre;
+
base.AddNestedHitObject(hitObject);
dropletContainer.Add(hitObject);
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
index 5797588ded..7ac9f11ad6 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
@@ -7,6 +7,7 @@ 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;
@@ -67,7 +68,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- BorderColour = Color4.Red,
+ BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
BorderThickness = 12f * RADIUS_ADJUST,
Children = new Drawable[]
{
@@ -77,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
Alpha = 0.3f,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
- Colour = Color4.Red,
+ Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
}
}
});
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 642ff0246e..6b8b70ed54 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -3,17 +3,17 @@
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.
@@ -24,8 +24,8 @@ namespace osu.Game.Rulesets.Catch.Objects
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 .
@@ -45,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",
@@ -58,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)
@@ -73,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,
});
}
}
@@ -93,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;
@@ -106,22 +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
+ public float EndX => X + this.CurvePositionAt(1).X;
+
+ public double Duration
{
- get => StartTime + this.SpanCount() * Path.Distance / Velocity;
+ get => this.SpanCount() * Path.Distance / Velocity;
set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
}
- public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH;
-
- 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 4649dcae90..5d11c574b1 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
@@ -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/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
index 36164c5543..d929da1a29 100644
--- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
@@ -2,26 +2,21 @@
// See the LICENCE file in the repository root for full licence text.
using Humanizer;
-using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Textures;
-using osu.Game.Audio;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning
{
- public class CatchLegacySkinTransformer : ISkin
+ public class CatchLegacySkinTransformer : LegacySkinTransformer
{
- private readonly ISkin source;
-
public CatchLegacySkinTransformer(ISkinSource source)
+ : base(source)
{
- this.source = source;
}
- public Drawable GetDrawableComponent(ISkinComponent component)
+ public override Drawable GetDrawableComponent(ISkinComponent component)
{
if (!(component is CatchSkinComponent catchSkinComponent))
return null;
@@ -44,15 +39,32 @@ namespace osu.Game.Rulesets.Catch.Skinning
return new LegacyFruitPiece("fruit-drop") { Scale = new Vector2(0.8f) };
break;
+
+ case CatchSkinComponents.CatcherIdle:
+ return this.GetAnimation("fruit-catcher-idle", true, true, true) ??
+ this.GetAnimation("fruit-ryuuta", true, true, true);
+
+ case CatchSkinComponents.CatcherFail:
+ return this.GetAnimation("fruit-catcher-fail", true, true, true) ??
+ this.GetAnimation("fruit-ryuuta", true, true, true);
+
+ case CatchSkinComponents.CatcherKiai:
+ return this.GetAnimation("fruit-catcher-kiai", true, true, true) ??
+ this.GetAnimation("fruit-ryuuta", true, true, true);
}
return null;
}
- public Texture GetTexture(string componentName) => source.GetTexture(componentName);
+ public override IBindable GetConfig(TLookup lookup)
+ {
+ switch (lookup)
+ {
+ case CatchSkinColour colour:
+ return Source.GetConfig(new SkinCustomColourLookup(colour));
+ }
- public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample);
-
- public IBindable GetConfig(TLookup lookup) => source.GetConfig(lookup);
+ 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
index 25ee0811d0..5be54d3882 100644
--- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
@@ -7,6 +7,7 @@ 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;
@@ -55,14 +56,16 @@ namespace osu.Game.Rulesets.Catch.Skinning
{
var hyperDash = new Sprite
{
- Texture = skin.GetTexture(lookupName),
- Colour = Color4.Red,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Blending = BlendingParameters.Additive,
Depth = 1,
Alpha = 0.7f,
- Scale = new Vector2(1.2f)
+ 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);
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index 2319c5ac1f..d034f3c7d4 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -16,7 +16,16 @@ 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;
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..82cbbefcca
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -0,0 +1,477 @@
+// 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.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.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;
+
+ [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 Container caughtFruit;
+
+ 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;
+
+ 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()
+ {
+ InternalChildren = new Drawable[]
+ {
+ caughtFruit = new Container
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.BottomCentre,
+ },
+ catcherIdle = new CatcherSprite(CatcherAnimationState.Idle)
+ {
+ Anchor = Anchor.TopCentre,
+ Alpha = 0,
+ },
+ catcherKiai = new CatcherSprite(CatcherAnimationState.Kiai)
+ {
+ Anchor = Anchor.TopCentre,
+ Alpha = 0,
+ },
+ catcherFail = new CatcherSprite(CatcherAnimationState.Fail)
+ {
+ Anchor = Anchor.TopCentre,
+ Alpha = 0,
+ }
+ };
+
+ trailsTarget.Add(trails = new CatcherTrailDisplay(this));
+
+ updateCatcher();
+ }
+
+ ///
+ /// 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 (caughtFruit.Any(f =>
+ f.LifetimeEnd == double.MaxValue &&
+ Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2)))
+ {
+ var diff = (ourRadius + theirRadius) / allowance;
+ fruit.X += (RNG.NextSingle() - 0.5f) * diff * 2;
+ fruit.Y -= RNG.NextSingle() * diff;
+ }
+
+ fruit.X = Math.Clamp(fruit.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
+
+ caughtFruit.Add(fruit);
+
+ 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 caughtFruit.ToArray())
+ Drop(f);
+ }
+
+ ///
+ /// Explode any fruit off the plate.
+ ///
+ public void Explode()
+ {
+ foreach (var f in caughtFruit.ToArray())
+ Explode(f);
+ }
+
+ public void Drop(DrawableHitObject fruit)
+ {
+ removeFromPlateWithTransform(fruit, f =>
+ {
+ f.MoveToY(f.Y + 75, 750, Easing.InSine);
+ f.FadeOut(750);
+ });
+ }
+
+ public void Explode(DrawableHitObject fruit)
+ {
+ var originalX = fruit.X * Scale.X;
+
+ removeFromPlateWithTransform(fruit, f =>
+ {
+ f.MoveToY(f.Y - 50, 250, Easing.OutSine).Then().MoveToY(f.Y + 50, 500, Easing.InSine);
+ f.MoveToX(f.X + originalX * 6, 1000);
+ f.FadeOut(750);
+ });
+ }
+
+ protected override void 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 = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
+
+ if (!caughtFruit.Remove(fruit))
+ // we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling).
+ // this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice.
+ return;
+
+ ExplodingFruitTarget.Add(fruit);
+ }
+
+ var actionTime = Clock.CurrentTime;
+
+ fruit.ApplyCustomUpdateState += onFruitOnApplyCustomUpdateState;
+ onFruitOnApplyCustomUpdateState(fruit, fruit.State.Value);
+
+ void onFruitOnApplyCustomUpdateState(DrawableHitObject o, ArmedState state)
+ {
+ using (fruit.BeginAbsoluteSequence(actionTime))
+ action(fruit);
+
+ fruit.Expire();
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs b/osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs
new file mode 100644
index 0000000000..566e9d1911
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs
@@ -0,0 +1,12 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ public enum CatcherAnimationState
+ {
+ Idle,
+ Fail,
+ Kiai
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index b977d46611..bf1ac5bc0e 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -2,14 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Linq;
-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.Input.Bindings;
-using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
@@ -19,7 +13,6 @@ 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
{
@@ -27,8 +20,6 @@ namespace osu.Game.Rulesets.Catch.UI
{
public const float CATCHER_SIZE = 106.75f;
- protected internal readonly Catcher MovableCatcher;
-
public Func> CreateDrawableRepresentation;
public Container ExplodingFruitTarget
@@ -36,18 +27,14 @@ namespace osu.Game.Rulesets.Catch.UI
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)
@@ -99,6 +86,15 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
+ public void OnReleased(CatchAction action)
+ {
+ }
+
+ public bool AttemptCatch(CatchHitObject obj)
+ {
+ return MovableCatcher.AttemptCatch(obj);
+ }
+
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
@@ -109,479 +105,6 @@ namespace osu.Game.Rulesets.Catch.UI
MovableCatcher.X = state.CatcherX.Value;
}
- public void OnReleased(CatchAction action)
- {
- }
-
- public bool AttemptCatch(CatchHitObject obj) => MovableCatcher.AttemptCatch(obj);
-
- public static float GetCatcherSize(BeatmapDifficulty difficulty)
- {
- return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
- }
-
- public class Catcher : Container, IKeyBindingHandler
- {
- ///
- /// Width of the area that can be used to attempt catches during gameplay.
- ///
- internal float CatchWidth => CATCHER_SIZE * Math.Abs(Scale.X);
-
- private Container caughtFruit;
-
- public Container ExplodingFruitTarget;
-
- public Container AdditiveTarget;
-
- public Catcher(BeatmapDifficulty difficulty = null)
- {
- RelativePositionAxes = Axes.X;
- X = 0.5f;
-
- Origin = Anchor.TopCentre;
-
- 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 PlaceOnPlate(DrawableCatchHitObject fruit)
- {
- float ourRadius = fruit.DisplayRadius;
- float theirRadius = 0;
-
- const float allowance = 6;
-
- while (caughtFruit.Any(f =>
- f.LifetimeEnd == double.MaxValue &&
- Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2)))
- {
- 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);
-
- Add(new HitExplosion(fruit)
- {
- X = fruit.X,
- Scale = new Vector2(fruit.HitObject.Scale)
- });
- }
-
- ///
- /// Let the catcher attempt to catch a fruit.
- ///
- /// The fruit to catch.
- /// Whether the catch is possible.
- public bool AttemptCatch(CatchHitObject fruit)
- {
- float halfCatchWidth = CatchWidth * 0.5f;
-
- // this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
- var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH;
- var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH;
-
- var validCatch =
- catchObjectPosition >= catcherPosition - halfCatchWidth &&
- catchObjectPosition <= catcherPosition + halfCatchWidth;
-
- if (validCatch && fruit.HyperDash)
- {
- var target = fruit.HyperDashTarget;
- double timeDifference = target.StartTime - fruit.StartTime;
- double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition;
- double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
-
- SetHyperDashState(Math.Abs(velocity), target.X);
- }
- else
- {
- SetHyperDashState();
- }
-
- return validCatch;
- }
-
- private double hyperDashModifier = 1;
- private int hyperDashDirection;
- private float hyperDashTargetPosition;
-
- ///
- /// Whether we are hyper-dashing or not.
- ///
- public bool HyperDashing => hyperDashModifier != 1;
-
- ///
- /// Set hyper-dash state.
- ///
- /// The speed multiplier. If this is less or equals to 1, this catcher will be non-hyper-dashing state.
- /// When this catcher crosses this position, this catcher ends hyper-dashing.
- public void SetHyperDashState(double modifier = 1, float targetPosition = -1)
- {
- const float hyper_dash_transition_length = 180;
-
- bool previouslyHyperDashing = HyperDashing;
-
- if (modifier <= 1 || X == targetPosition)
- {
- hyperDashModifier = 1;
- hyperDashDirection = 0;
-
- if (previouslyHyperDashing)
- {
- this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint);
- this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint);
- Trail &= Dashing;
- }
- }
- else
- {
- hyperDashModifier = modifier;
- hyperDashDirection = Math.Sign(targetPosition - X);
- hyperDashTargetPosition = targetPosition;
-
- if (!previouslyHyperDashing)
- {
- this.FadeColour(Color4.OrangeRed, hyper_dash_transition_length, Easing.OutQuint);
- this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint);
- Trail = true;
- }
- }
- }
-
- public bool OnPressed(CatchAction action)
- {
- switch (action)
- {
- case CatchAction.MoveLeft:
- currentDirection--;
- return true;
-
- case CatchAction.MoveRight:
- currentDirection++;
- return true;
-
- case CatchAction.Dash:
- Dashing = true;
- return true;
- }
-
- return false;
- }
-
- public void OnReleased(CatchAction action)
- {
- switch (action)
- {
- case CatchAction.MoveLeft:
- currentDirection++;
- break;
-
- case CatchAction.MoveRight:
- currentDirection--;
- break;
-
- case CatchAction.Dash:
- Dashing = false;
- break;
- }
- }
-
- ///
- /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
- ///
- public const double BASE_SPEED = 1.0 / 512;
-
- protected override void Update()
- {
- base.Update();
-
- if (currentDirection == 0) return;
-
- var direction = Math.Sign(currentDirection);
-
- double dashModifier = Dashing ? 1 : 0.5;
- double speed = BASE_SPEED * dashModifier * hyperDashModifier;
-
- UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed));
-
- // Correct overshooting.
- if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
- (hyperDashDirection < 0 && hyperDashTargetPosition > X))
- {
- X = hyperDashTargetPosition;
- SetHyperDashState();
- }
- }
-
- public void UpdatePosition(float position)
- {
- position = Math.Clamp(position, 0, 1);
-
- if (position == X)
- return;
-
- Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y);
- X = position;
- }
-
- ///
- /// Drop any fruit off the plate.
- ///
- public void Drop()
- {
- foreach (var f in caughtFruit.ToArray())
- Drop(f);
- }
-
- ///
- /// Explode any fruit off the plate.
- ///
- public void Explode()
- {
- foreach (var f in caughtFruit.ToArray())
- Explode(f);
- }
-
- public void Drop(DrawableHitObject fruit) => removeFromPlateWithTransform(fruit, f =>
- {
- f.MoveToY(f.Y + 75, 750, Easing.InSine);
- f.FadeOut(750);
- });
-
- public void Explode(DrawableHitObject fruit)
- {
- var originalX = fruit.X * Scale.X;
-
- removeFromPlateWithTransform(fruit, f =>
- {
- f.MoveToY(f.Y - 50, 250, Easing.OutSine).Then().MoveToY(f.Y + 50, 500, Easing.InSine);
- f.MoveToX(f.X + originalX * 6, 1000);
- f.FadeOut(750);
- });
- }
-
- private void removeFromPlateWithTransform(DrawableHitObject fruit, Action action)
- {
- if (ExplodingFruitTarget != null)
- {
- fruit.Anchor = Anchor.TopLeft;
- fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
-
- if (!caughtFruit.Remove(fruit))
- // we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling).
- // this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice.
- return;
-
- ExplodingFruitTarget.Add(fruit);
- }
-
- double 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();
- }
- }
- }
- }
-
- public class HitExplosion : CompositeDrawable
- {
- private readonly CircularContainer largeFaint;
-
- public HitExplosion(DrawableCatchHitObject fruit)
- {
- Size = new Vector2(20);
- Anchor = Anchor.TopCentre;
- Origin = Anchor.BottomCentre;
-
- Color4 objectColour = fruit.AccentColour.Value;
-
- // scale roughly in-line with visual appearance of notes
-
- const float angle_variangle = 15; // should be less than 45
-
- const float roundness = 100;
-
- const float initial_height = 10;
-
- var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1);
-
- InternalChildren = new Drawable[]
- {
- largeFaint = new CircularContainer
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- // we want our size to be very small so the glow dominates it.
- Size = new Vector2(0.8f),
- Blending = BlendingParameters.Additive,
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
- Roundness = 160,
- Radius = 200,
- },
- },
- new CircularContainer
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- Blending = BlendingParameters.Additive,
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
- Roundness = 20,
- Radius = 50,
- },
- },
- new CircularContainer
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- Size = new Vector2(0.01f, initial_height),
- Blending = BlendingParameters.Additive,
- Rotation = RNG.NextSingle(-angle_variangle, angle_variangle),
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = colour,
- Roundness = roundness,
- Radius = 40,
- },
- },
- new CircularContainer
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- Size = new Vector2(0.01f, initial_height),
- Blending = BlendingParameters.Additive,
- Rotation = RNG.NextSingle(-angle_variangle, angle_variangle),
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = colour,
- Roundness = roundness,
- Radius = 40,
- },
- }
- };
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- const double duration = 400;
-
- largeFaint
- .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
- .FadeOut(duration * 2);
-
- this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out);
- Expire(true);
- }
+ protected internal readonly Catcher MovableCatcher;
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
index 025fa9c56e..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 fd8a1d175d..ebe45aa3ab 100644
--- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
@@ -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 84%
rename from osu.Game.Rulesets.Mania/UI/HitExplosion.cs
rename to osu.Game.Rulesets.Catch/UI/HitExplosion.cs
index 35de47e208..04a86f83be 100644
--- a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs
+++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
@@ -6,33 +6,29 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Utils;
-using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
+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/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/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 692d079c16..0000000000
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs
+++ /dev/null
@@ -1,38 +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.Tests.Visual;
-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/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 6855b99f28..77c871718b 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/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 d904474815..b025ac7992 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -6,6 +6,7 @@ using System;
using System.Linq;
using System.Collections.Generic;
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 315ef96e49..d03eb0b3c9 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -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 7bbde400ea..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 (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 a3657d3bb9..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 void OnMouseUp(MouseUpEvent e)
+ public override void UpdatePosition(SnapResult result)
{
- EndPlacement(true);
- base.OnMouseUp(e);
- }
+ base.UpdatePosition(result);
- public override void UpdatePosition(Vector2 screenSpacePosition)
- {
if (!PlacementActive)
- 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;
+ 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 9f57160f99..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 : OverlaySelectionBlueprint
+ 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,22 +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 void OnDrag(DragEvent e)
- {
- base.OnDrag(e);
-
- ScreenSpaceDragPosition = e.ScreenSpaceMousePosition;
- DragPosition = DrawableObject.ToLocalSpace(e.ScreenSpaceMousePosition);
- }
-
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
index d744036b4c..cea27498c3 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
@@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mania.Edit
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 62b609610f..7e2469a794 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
@@ -6,38 +6,78 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Objects;
using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
+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)
{
@@ -49,12 +89,36 @@ namespace osu.Game.Rulesets.Mania.Edit
return drawableRuleset;
}
- protected override ComposeBlueprintContainer CreateBlueprintContainer() => new ManiaBlueprintContainer(drawableRuleset.Playfield.AllHitObjects);
+ protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects)
+ => new ManiaBlueprintContainer(hitObjects);
protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[]
{
new NoteCompositionTool(),
new HoldNoteCompositionTool()
};
+
+ protected override void UpdateAfterChildren()
+ {
+ base.UpdateAfterChildren();
+
+ if (BlueprintContainer.CurrentTool is SelectTool)
+ {
+ 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;
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index 9069a636a8..65f40d7d0a 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
@@ -4,11 +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;
@@ -20,90 +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.Parent.DrawHeight; // todo: probably wrong
-
- foreach (var selectionBlueprint in SelectedBlueprints)
- {
- var b = (OverlaySelectionBlueprint)selectionBlueprint;
-
- 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;
@@ -122,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 433db79ae0..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 : OverlaySelectionBlueprint
- {
- protected ManiaSelectionBlueprint(DrawableHitObject drawableObject)
- : base(drawableObject)
- {
- RelativeSizeAxes = Axes.None;
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
index 00b839f8ec..294aab1e4e 100644
--- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
+++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
@@ -7,8 +7,6 @@ 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)
diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
index c2f8fb8678..53db676a54 100644
--- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
@@ -25,8 +25,10 @@ namespace osu.Game.Rulesets.Mania.Judgements
return 200;
case HitResult.Great:
- case HitResult.Perfect:
return 300;
+
+ case HitResult.Perfect:
+ 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 b7b523a94d..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()),
@@ -197,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;
}
}
@@ -209,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.Q,
- InputKey.W,
- InputKey.E,
- InputKey.R,
- },
- RightKeys = new[]
- {
- InputKey.X,
- InputKey.C,
- InputKey.V,
- InputKey.B
- },
- SpecialKey = InputKey.S,
- 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.K,
- InputKey.L,
- InputKey.Semicolon,
- InputKey.Quote
- },
- SpecialKey = InputKey.I,
- SpecialAction = ManiaAction.Special2,
- NormalActionStart = nextNormal
- }.GenerateKeyBindingsFor(keys, out _);
-
- return stage1Bindings.Concat(stage2Bindings);
+ return new DualStageVariantGenerator(getDualStageKeyCount(variant)).GenerateMappings();
}
return Array.Empty();
@@ -312,58 +313,20 @@ namespace osu.Game.Rulesets.Mania
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderByDescending(i => i).First(v => variant >= v);
}
- private class VariantMappingGenerator
+ public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
{
- ///
- /// All the s available to the left hand.
- ///
- public InputKey[] LeftKeys;
-
- ///
- /// All the s available to the right hand.
- ///
- public InputKey[] RightKeys;
-
- ///
- /// The for the special key.
- ///
- public InputKey SpecialKey;
-
- ///
- /// The at which the normal columns should begin.
- ///
- public ManiaAction NormalActionStart;
-
- ///
- /// The for the special column.
- ///
- public ManiaAction SpecialAction;
-
- ///
- /// Generates a list of s for a specific number of columns.
- ///
- /// The number of columns that need to be bound.
- /// The next to use for normal columns.
- /// The keybindings.
- public IEnumerable GenerateKeyBindingsFor(int columns, out ManiaAction nextNormalAction)
+ new StatisticRow
{
- ManiaAction currentNormalAction = NormalActionStart;
-
- var bindings = new List();
-
- for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++)
- bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++));
-
- if (columns % 2 == 1)
- bindings.Add(new KeyBinding(SpecialKey, SpecialAction));
-
- for (int i = 0; i < columns / 2; i++)
- bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++));
-
- nextNormalAction = currentNormalAction;
- return bindings;
+ Columns = new[]
+ {
+ new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents)
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }),
+ }
}
- }
+ };
}
public enum PlayfieldType
diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
index 69bd4b0ecf..c0c8505f44 100644
--- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
@@ -1,19 +1,46 @@
// 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 osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania
{
public class ManiaSkinComponent : GameplaySkinComponent
{
- public ManiaSkinComponent(ManiaSkinComponents component)
+ ///
+ /// The intended index for this component.
+ /// May be null if the component does not exist in a .
+ ///
+ public readonly int? TargetColumn;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The component.
+ /// The intended index for this component. May be null if the component does not exist in a .
+ public ManiaSkinComponent(ManiaSkinComponents component, int? targetColumn = null)
: base(component)
{
+ TargetColumn = targetColumn;
}
protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME;
protected override string ComponentName => Component.ToString().ToLower();
}
+
+ public enum ManiaSkinComponents
+ {
+ ColumnBackground,
+ HitTarget,
+ KeyArea,
+ Note,
+ HoldNoteHead,
+ HoldNoteTail,
+ HoldNoteBody,
+ HitExplosion,
+ StageBackground,
+ StageForeground,
+ }
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
index 6893e1e73b..86a00271e9 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
@@ -3,8 +3,8 @@
using System;
using osu.Framework.Bindables;
-using osu.Framework.Caching;
using osu.Framework.Graphics;
+using osu.Framework.Layout;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
using osuTK;
@@ -22,21 +22,13 @@ namespace osu.Game.Rulesets.Mania.Mods
private class ManiaFlashlight : Flashlight
{
- private readonly Cached flashlightProperties = new Cached();
+ private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize);
public ManiaFlashlight()
{
FlashlightSize = new Vector2(0, default_flashlight_size);
- }
- public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true)
- {
- if ((invalidation & Invalidation.DrawSize) > 0)
- {
- flashlightProperties.Invalidate();
- }
-
- return base.Invalidate(invalidation, source, shallPropagate);
+ AddLayout(flashlightProperties);
}
protected override void Update()
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs
new file mode 100644
index 0000000000..684370fc3d
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs
@@ -0,0 +1,13 @@
+// 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.Mods
+{
+ public class ManiaModKey10 : ManiaKeyMod
+ {
+ public override int KeyCount => 10;
+ public override string Name => "Ten Keys";
+ public override string Acronym => "10K";
+ public override string Description => @"Play with ten keys.";
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs
index 14b36fb765..699c58c373 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs
@@ -3,24 +3,17 @@
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
-using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
-using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
{
- public class ManiaModRandom : Mod, IApplicableToBeatmap
+ public class ManiaModRandom : ModRandom, IApplicableToBeatmap
{
- public override string Name => "Random";
- public override string Acronym => "RD";
- public override ModType Type => ModType.Conversion;
- public override IconUsage? Icon => OsuIcon.Dice;
public override string Description => @"Shuffle around the keys!";
- public override double ScoreMultiplier => 1;
public void ApplyToBeatmap(IBeatmap beatmap)
{
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 14a7c5fda3..2262bd2b7d 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -10,6 +10,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
@@ -20,6 +21,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
public override bool DisplayResult => false;
+ public IBindable IsHitting => isHitting;
+
+ private readonly Bindable isHitting = new Bindable();
+
public DrawableHoldNoteHead Head => headContainer.Child;
public DrawableHoldNoteTail Tail => tailContainer.Child;
@@ -27,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
private readonly Container tailContainer;
private readonly Container tickContainer;
- private readonly BodyPiece bodyPiece;
+ private readonly Drawable bodyPiece;
///
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
@@ -44,18 +49,19 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
RelativeSizeAxes = Axes.X;
- AddRangeInternal(new Drawable[]
+ AddRangeInternal(new[]
{
- bodyPiece = new BodyPiece { RelativeSizeAxes = Axes.X },
+ bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece
+ {
+ RelativeSizeAxes = Axes.Both
+ })
+ {
+ RelativeSizeAxes = Axes.X
+ },
tickContainer = new Container { RelativeSizeAxes = Axes.Both },
headContainer = new Container { RelativeSizeAxes = Axes.Both },
tailContainer = new Container { RelativeSizeAxes = Axes.Both },
});
-
- AccentColour.BindValueChanged(colour =>
- {
- bodyPiece.AccentColour = colour.NewValue;
- }, true);
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
@@ -124,6 +130,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
}
+ public override void PlaySamples()
+ {
+ // Samples are played by the head/tail notes.
+ }
+
protected override void Update()
{
base.Update();
@@ -168,7 +179,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
return;
HoldStartTime = Time.Current;
- bodyPiece.Hitting = true;
+ isHitting.Value = true;
}
public void OnReleased(ManiaAction action)
@@ -194,7 +205,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
private void endHold()
{
HoldStartTime = null;
- bodyPiece.Hitting = false;
+ isHitting.Value = false;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
index 390c64c5e2..a73fe259e4 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
@@ -8,6 +8,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
public class DrawableHoldNoteHead : DrawableNote
{
+ protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteHead;
+
public DrawableHoldNoteHead(DrawableHoldNote holdNote)
: base(holdNote.HitObject.Head)
{
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
index 568b07c958..31e43d3ee2 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
@@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
private const double release_window_lenience = 1.5;
+ protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail;
+
private readonly DrawableHoldNote holdNote;
public DrawableHoldNoteTail(DrawableHoldNote holdNote)
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index 5bfa07bd14..a44d8b09aa 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -7,16 +7,12 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
public abstract class DrawableManiaHitObject : DrawableHitObject
{
- ///
- /// Whether this should always remain alive.
- ///
- internal bool AlwaysAlive;
-
///
/// The which causes this to be hit.
///
@@ -24,6 +20,20 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected readonly IBindable Direction = new Bindable();
+ [Resolved(canBeNull: true)]
+ private ManiaPlayfield playfield { get; set; }
+
+ protected override float SamplePlaybackPosition
+ {
+ get
+ {
+ if (playfield == null)
+ return base.SamplePlaybackPosition;
+
+ return (float)HitObject.Column / playfield.TotalColumns;
+ }
+ }
+
protected DrawableManiaHitObject(ManiaHitObject hitObject)
: base(hitObject)
{
@@ -39,7 +49,62 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Direction.BindValueChanged(OnDirectionChanged, true);
}
- protected override bool ShouldBeAlive => AlwaysAlive || base.ShouldBeAlive;
+ private double computedLifetimeStart;
+
+ public override double LifetimeStart
+ {
+ get => base.LifetimeStart;
+ set
+ {
+ computedLifetimeStart = value;
+
+ if (!AlwaysAlive)
+ base.LifetimeStart = value;
+ }
+ }
+
+ private double computedLifetimeEnd;
+
+ public override double LifetimeEnd
+ {
+ get => base.LifetimeEnd;
+ set
+ {
+ computedLifetimeEnd = value;
+
+ if (!AlwaysAlive)
+ base.LifetimeEnd = value;
+ }
+ }
+
+ private bool alwaysAlive;
+
+ ///
+ /// Whether this should always remain alive.
+ ///
+ internal bool AlwaysAlive
+ {
+ get => alwaysAlive;
+ set
+ {
+ if (alwaysAlive == value)
+ return;
+
+ alwaysAlive = value;
+
+ if (value)
+ {
+ // Set the base lifetimes directly, to avoid mangling the computed lifetimes
+ base.LifetimeStart = double.MinValue;
+ base.LifetimeEnd = double.MaxValue;
+ }
+ else
+ {
+ LifetimeStart = computedLifetimeStart;
+ LifetimeEnd = computedLifetimeEnd;
+ }
+ }
+ }
protected virtual void OnDirectionChanged(ValueChangedEvent e)
{
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
index 85613d3afb..9451bc4430 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
@@ -3,13 +3,12 @@
using System.Diagnostics;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Effects;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
@@ -18,7 +17,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
public class DrawableNote : DrawableManiaHitObject, IKeyBindingHandler
{
- private readonly NotePiece headPiece;
+ protected virtual ManiaSkinComponents Component => ManiaSkinComponents.Note;
+
+ private readonly Drawable headPiece;
public DrawableNote(Note hitObject)
: base(hitObject)
@@ -26,22 +27,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
- CornerRadius = 5;
- Masking = true;
-
- AddInternal(headPiece = new NotePiece());
-
- AccentColour.BindValueChanged(colour =>
+ AddInternal(headPiece = new SkinnableDrawable(new ManiaSkinComponent(Component, hitObject.Column), _ => new DefaultNotePiece())
{
- headPiece.AccentColour = colour.NewValue;
-
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = colour.NewValue.Lighten(1f).Opacity(0.2f),
- Radius = 10,
- };
- }, true);
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ });
}
protected override void OnDirectionChanged(ValueChangedEvent e)
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs
similarity index 66%
rename from osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs
rename to osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs
index 31a4857805..bc4a095395 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs
@@ -2,33 +2,47 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osu.Framework.Caching;
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics;
+using osu.Framework.Layout;
+using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
{
///
/// Represents length-wise portion of a hold note.
///
- public class BodyPiece : Container, IHasAccentColour
+ public class DefaultBodyPiece : CompositeDrawable
{
- private readonly Container subtractionLayer;
+ protected readonly Bindable AccentColour = new Bindable();
- protected readonly Drawable Background;
- protected readonly BufferedContainer Foreground;
- private readonly BufferedContainer subtractionContainer;
+ private readonly LayoutValue subtractionCache = new LayoutValue(Invalidation.DrawSize);
+ private readonly IBindable isHitting = new Bindable();
- public BodyPiece()
+ protected Drawable Background { get; private set; }
+ protected BufferedContainer Foreground { get; private set; }
+
+ private BufferedContainer subtractionContainer;
+ private Container subtractionLayer;
+
+ public DefaultBodyPiece()
{
Blending = BlendingParameters.Additive;
- Children = new[]
+ AddLayout(subtractionCache);
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load([CanBeNull] DrawableHitObject drawableObject)
+ {
+ InternalChildren = new[]
{
Background = new Box { RelativeSizeAxes = Axes.Both },
Foreground = new BufferedContainer
@@ -65,49 +79,37 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
}
}
};
- }
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- updateAccentColour();
- }
-
- private Color4 accentColour;
-
- public Color4 AccentColour
- {
- get => accentColour;
- set
+ if (drawableObject != null)
{
- if (accentColour == value)
- return;
+ var holdNote = (DrawableHoldNote)drawableObject;
- accentColour = value;
-
- updateAccentColour();
+ AccentColour.BindTo(drawableObject.AccentColour);
+ isHitting.BindTo(holdNote.IsHitting);
}
+
+ AccentColour.BindValueChanged(onAccentChanged, true);
+ isHitting.BindValueChanged(_ => onAccentChanged(new ValueChangedEvent(AccentColour.Value, AccentColour.Value)), true);
}
- public bool Hitting
+ private void onAccentChanged(ValueChangedEvent accent)
{
- get => hitting;
- set
+ Foreground.Colour = accent.NewValue.Opacity(0.5f);
+ Background.Colour = accent.NewValue.Opacity(0.7f);
+
+ const float animation_length = 50;
+
+ Foreground.ClearTransforms(false, nameof(Foreground.Colour));
+
+ if (isHitting.Value)
{
- hitting = value;
- updateAccentColour();
+ // wait for the next sync point
+ double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
+ using (Foreground.BeginDelayedSequence(synchronisedOffset))
+ Foreground.FadeColour(accent.NewValue.Lighten(0.2f), animation_length).Then().FadeColour(Foreground.Colour, animation_length).Loop();
}
- }
- private readonly Cached subtractionCache = new Cached();
-
- public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true)
- {
- if ((invalidation & Invalidation.DrawSize) > 0)
- subtractionCache.Invalidate();
-
- return base.Invalidate(invalidation, source, shallPropagate);
+ subtractionCache.Invalidate();
}
protected override void Update()
@@ -131,30 +133,5 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
subtractionCache.Validate();
}
}
-
- private bool hitting;
-
- private void updateAccentColour()
- {
- if (!IsLoaded)
- return;
-
- Foreground.Colour = AccentColour.Opacity(0.5f);
- Background.Colour = AccentColour.Opacity(0.7f);
-
- const float animation_length = 50;
-
- Foreground.ClearTransforms(false, nameof(Foreground.Colour));
-
- if (hitting)
- {
- // wait for the next sync point
- double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
- using (Foreground.BeginDelayedSequence(synchronisedOffset))
- Foreground.FadeColour(AccentColour.Lighten(0.2f), animation_length).Then().FadeColour(Foreground.Colour, animation_length).Loop();
- }
-
- subtractionCache.Invalidate();
- }
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultNotePiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultNotePiece.cs
new file mode 100644
index 0000000000..29f5217fd8
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultNotePiece.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 JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osuTK.Graphics;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+
+namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
+{
+ ///
+ /// Represents the static hit markers of notes.
+ ///
+ internal class DefaultNotePiece : CompositeDrawable
+ {
+ public const float NOTE_HEIGHT = 12;
+
+ private readonly IBindable direction = new Bindable();
+ private readonly IBindable accentColour = new Bindable();
+
+ private readonly Box colouredBox;
+
+ public DefaultNotePiece()
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = NOTE_HEIGHT;
+
+ CornerRadius = 5;
+ Masking = true;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ colouredBox = new Box
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = NOTE_HEIGHT / 2,
+ Alpha = 0.1f
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load([NotNull] IScrollingInfo scrollingInfo, [CanBeNull] DrawableHitObject drawableObject)
+ {
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+
+ if (drawableObject != null)
+ {
+ accentColour.BindTo(drawableObject.AccentColour);
+ accentColour.BindValueChanged(onAccentChanged, true);
+ }
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ colouredBox.Anchor = colouredBox.Origin = direction.NewValue == ScrollingDirection.Up
+ ? Anchor.TopCentre
+ : Anchor.BottomCentre;
+ }
+
+ private void onAccentChanged(ValueChangedEvent accent)
+ {
+ colouredBox.Colour = accent.NewValue.Lighten(0.9f);
+
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = accent.NewValue.Lighten(1f).Opacity(0.2f),
+ Radius = 10,
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs
deleted file mode 100644
index 4521af7dfb..0000000000
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs
+++ /dev/null
@@ -1,73 +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.Bindables;
-using osuTK.Graphics;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics;
-using osu.Game.Rulesets.UI.Scrolling;
-
-namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
-{
- ///
- /// Represents the static hit markers of notes.
- ///
- internal class NotePiece : Container, IHasAccentColour
- {
- public const float NOTE_HEIGHT = 12;
-
- private readonly IBindable direction = new Bindable();
-
- private readonly Box colouredBox;
-
- public NotePiece()
- {
- RelativeSizeAxes = Axes.X;
- Height = NOTE_HEIGHT;
-
- Children = new[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both
- },
- colouredBox = new Box
- {
- RelativeSizeAxes = Axes.X,
- Height = NOTE_HEIGHT / 2,
- Alpha = 0.1f
- }
- };
- }
-
- [BackgroundDependencyLoader]
- private void load(IScrollingInfo scrollingInfo)
- {
- direction.BindTo(scrollingInfo.Direction);
- direction.BindValueChanged(dir =>
- {
- colouredBox.Anchor = colouredBox.Origin = dir.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
- }, true);
- }
-
- private Color4 accentColour;
-
- public Color4 AccentColour
- {
- get => accentColour;
- set
- {
- if (accentColour == value)
- return;
-
- accentColour = value;
-
- colouredBox.Colour = AccentColour.Lighten(0.9f);
- }
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
index 049bf55f90..a100c9a58e 100644
--- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
@@ -1,6 +1,9 @@
// 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.Threading;
+using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
@@ -12,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Objects
///
/// Represents a hit object which requires pressing, holding, and releasing a key.
///
- public class HoldNote : ManiaHitObject, IHasEndTime
+ public class HoldNote : ManiaHitObject, IHasDuration
{
public double EndTime
{
@@ -28,7 +31,9 @@ namespace osu.Game.Rulesets.Mania.Objects
set
{
duration = value;
- Tail.StartTime = EndTime;
+
+ if (Tail != null)
+ Tail.StartTime = EndTime;
}
}
@@ -38,8 +43,12 @@ namespace osu.Game.Rulesets.Mania.Objects
set
{
base.StartTime = value;
- Head.StartTime = value;
- Tail.StartTime = EndTime;
+
+ if (Head != null)
+ Head.StartTime = value;
+
+ if (Tail != null)
+ Tail.StartTime = EndTime;
}
}
@@ -49,20 +58,26 @@ namespace osu.Game.Rulesets.Mania.Objects
set
{
base.Column = value;
- Head.Column = value;
- Tail.Column = value;
+
+ if (Head != null)
+ Head.Column = value;
+
+ if (Tail != null)
+ Tail.Column = value;
}
}
+ public List> NodeSamples { get; set; }
+
///
/// The head note of the hold.
///
- public readonly Note Head = new Note();
+ public Note Head { get; private set; }
///
/// The tail note of the hold.
///
- public readonly TailNote Tail = new TailNote();
+ public TailNote Tail { get; private set; }
///
/// The time between ticks of this hold.
@@ -77,23 +92,36 @@ namespace osu.Game.Rulesets.Mania.Objects
tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate;
}
- protected override void CreateNestedHitObjects()
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
- base.CreateNestedHitObjects();
+ base.CreateNestedHitObjects(cancellationToken);
- createTicks();
+ createTicks(cancellationToken);
- AddNested(Head);
- AddNested(Tail);
+ AddNested(Head = new Note
+ {
+ StartTime = StartTime,
+ Column = Column,
+ Samples = getNodeSamples(0),
+ });
+
+ AddNested(Tail = new TailNote
+ {
+ StartTime = EndTime,
+ Column = Column,
+ Samples = getNodeSamples((NodeSamples?.Count - 1) ?? 1),
+ });
}
- private void createTicks()
+ private void createTicks(CancellationToken cancellationToken)
{
if (tickSpacing == 0)
return;
for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
AddNested(new HoldNoteTick
{
StartTime = t,
@@ -105,5 +133,8 @@ namespace osu.Game.Rulesets.Mania.Objects
public override Judgement CreateJudgement() => new IgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
+
+ private IList getNodeSamples(int nodeIndex) =>
+ nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
index 995e1516cb..27bf50493d 100644
--- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
@@ -5,11 +5,12 @@ using osu.Framework.Bindables;
using osu.Game.Rulesets.Mania.Objects.Types;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects
{
- public abstract class ManiaHitObject : HitObject, IHasColumn
+ public abstract class ManiaHitObject : HitObject, IHasColumn, IHasXPosition
{
public readonly Bindable ColumnBindable = new Bindable();
@@ -20,5 +21,11 @@ namespace osu.Game.Rulesets.Mania.Objects
}
protected override HitWindows CreateHitWindows() => new ManiaHitWindows();
+
+ #region LegacyBeatmapEncoder
+
+ float IHasXPosition.X => Column;
+
+ #endregion
}
}
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
index 877a9ee410..dbab54d1d0 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
@@ -24,15 +25,9 @@ namespace osu.Game.Rulesets.Mania.Replays
Actions.AddRange(actions);
}
- public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
+ public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
{
- // We don't need to fully convert, just create the converter
- var converter = new ManiaBeatmapConverter(beatmap, new ManiaRuleset());
-
- // NB: Via co-op mod, osu-stable can have two stages with floor(col/2) and ceil(col/2) columns. This will need special handling
- // elsewhere in the game if we do choose to support the old co-op mod anyway. For now, assume that there is only one stage.
-
- var stage = new StageDefinition { Columns = converter.TargetColumns };
+ var maniaBeatmap = (ManiaBeatmap)beatmap;
var normalAction = ManiaAction.Key1;
var specialAction = ManiaAction.Special1;
@@ -42,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Replays
while (activeColumns > 0)
{
- var isSpecial = stage.IsSpecialColumn(counter);
+ bool isSpecial = isColumnAtIndexSpecial(maniaBeatmap, counter);
if ((activeColumns & 1) > 0)
Actions.Add(isSpecial ? specialAction : normalAction);
@@ -56,5 +51,94 @@ namespace osu.Game.Rulesets.Mania.Replays
activeColumns >>= 1;
}
}
+
+ public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
+ {
+ var maniaBeatmap = (ManiaBeatmap)beatmap;
+
+ int keys = 0;
+
+ foreach (var action in Actions)
+ {
+ switch (action)
+ {
+ case ManiaAction.Special1:
+ keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 0);
+ break;
+
+ case ManiaAction.Special2:
+ keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 1);
+ break;
+
+ default:
+ // the index in lazer, which doesn't include special keys.
+ int nonSpecialKeyIndex = action - ManiaAction.Key1;
+
+ // the index inclusive of special keys.
+ int overallIndex = 0;
+
+ // iterate to find the index including special keys.
+ for (; overallIndex < maniaBeatmap.TotalColumns; overallIndex++)
+ {
+ // skip over special columns.
+ if (isColumnAtIndexSpecial(maniaBeatmap, overallIndex))
+ continue;
+ // found a non-special column to use.
+ if (nonSpecialKeyIndex == 0)
+ break;
+ // found a non-special column but not ours.
+ nonSpecialKeyIndex--;
+ }
+
+ keys |= 1 << overallIndex;
+ break;
+ }
+ }
+
+ return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None);
+ }
+
+ ///
+ /// Find the overall index (across all stages) for a specified special key.
+ ///
+ /// The beatmap.
+ /// The special key offset (0 is S1).
+ /// The overall index for the special column.
+ private int getSpecialColumnIndex(ManiaBeatmap maniaBeatmap, int specialOffset)
+ {
+ for (int i = 0; i < maniaBeatmap.TotalColumns; i++)
+ {
+ if (isColumnAtIndexSpecial(maniaBeatmap, i))
+ {
+ if (specialOffset == 0)
+ return i;
+
+ specialOffset--;
+ }
+ }
+
+ throw new ArgumentException("Special key index is too high.", nameof(specialOffset));
+ }
+
+ ///
+ /// Check whether the column at an overall index (across all stages) is a special column.
+ ///
+ /// The beatmap.
+ /// The overall index to check.
+ private bool isColumnAtIndexSpecial(ManiaBeatmap beatmap, int index)
+ {
+ foreach (var stage in beatmap.Stages)
+ {
+ if (index >= stage.Columns)
+ {
+ index -= stage.Columns;
+ continue;
+ }
+
+ return stage.IsSpecialColumn(index);
+ }
+
+ throw new ArgumentException("Column index is too high.", nameof(index));
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json
new file mode 100644
index 0000000000..fec1360b26
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json
@@ -0,0 +1,33 @@
+{
+ "Mappings": [{
+ "StartTime": 1000.0,
+ "Objects": [{
+ "StartTime": 1000.0,
+ "EndTime": 2750.0,
+ "Column": 1,
+ "NodeSamples": [
+ ["normal-hitnormal"],
+ ["soft-hitnormal"],
+ ["drum-hitnormal"]
+ ],
+ "Samples": ["drum-hitnormal"]
+ }, {
+ "StartTime": 1875.0,
+ "EndTime": 2750.0,
+ "Column": 0,
+ "NodeSamples": [
+ ["soft-hitnormal"],
+ ["drum-hitnormal"]
+ ],
+ "Samples": ["drum-hitnormal"]
+ }]
+ }, {
+ "StartTime": 3750.0,
+ "Objects": [{
+ "StartTime": 3750.0,
+ "EndTime": 3750.0,
+ "Column": 3,
+ "Samples": ["normal-hitnormal"]
+ }]
+ }]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu
new file mode 100644
index 0000000000..fea1de6614
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu
@@ -0,0 +1,16 @@
+osu file format v14
+
+[Difficulty]
+HPDrainRate:5
+CircleSize:5
+OverallDifficulty:5
+ApproachRate:5
+SliderMultiplier:1.4
+SliderTickRate:1
+
+[TimingPoints]
+0,500,4,1,0,100,1,0
+
+[HitObjects]
+88,99,1000,6,0,L|306:259,2,245,0|0|0,1:0|2:0|3:0,0:0:0:0:
+259,118,3750,1,0,1:0:0:0:
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json
new file mode 100644
index 0000000000..1aca75a796
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json
@@ -0,0 +1,27 @@
+{
+ "Mappings": [{
+ "StartTime": 500.0,
+ "Objects": [{
+ "StartTime": 500.0,
+ "EndTime": 1500.0,
+ "Column": 0,
+ "NodeSamples": [
+ ["normal-hitnormal"],
+ []
+ ],
+ "Samples": ["normal-hitnormal"]
+ }]
+ }, {
+ "StartTime": 2000.0,
+ "Objects": [{
+ "StartTime": 2000.0,
+ "EndTime": 3000.0,
+ "Column": 2,
+ "NodeSamples": [
+ ["drum-hitnormal"],
+ []
+ ],
+ "Samples": ["drum-hitnormal"]
+ }]
+ }]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu
new file mode 100644
index 0000000000..7c75b45e5f
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu
@@ -0,0 +1,19 @@
+osu file format v14
+
+[General]
+Mode: 3
+
+[Difficulty]
+HPDrainRate:5
+CircleSize:5
+OverallDifficulty:5
+ApproachRate:5
+SliderMultiplier:1.4
+SliderTickRate:1
+
+[TimingPoints]
+0,500,4,1,0,100,1,0
+
+[HitObjects]
+51,192,500,128,0,1500:1:0:0:0:
+256,192,2000,128,0,3000:3:0:0:0:
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json
new file mode 100644
index 0000000000..e3768a90d7
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json
@@ -0,0 +1,21 @@
+{
+ "Mappings": [{
+ "StartTime": 8470.0,
+ "Objects": [{
+ "StartTime": 8470.0,
+ "EndTime": 8470.0,
+ "Column": 0,
+ "Samples": ["normal-hitnormal", "normal-hitclap"]
+ }, {
+ "StartTime": 8626.470587768974,
+ "EndTime": 8626.470587768974,
+ "Column": 1,
+ "Samples": ["normal-hitnormal"]
+ }, {
+ "StartTime": 8782.941175537948,
+ "EndTime": 8782.941175537948,
+ "Column": 2,
+ "Samples": ["normal-hitnormal", "normal-hitclap"]
+ }]
+ }]
+}
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples.osu
new file mode 100644
index 0000000000..08e90ce807
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples.osu
@@ -0,0 +1,15 @@
+osu file format v14
+
+[Difficulty]
+HPDrainRate:6
+CircleSize:4
+OverallDifficulty:8
+ApproachRate:9.5
+SliderMultiplier:2.00000000596047
+SliderTickRate:1
+
+[TimingPoints]
+0,312.941176470588,4,1,0,100,1,0
+
+[HitObjects]
+82,216,8470,6,0,P|52:161|99:113,2,100,8|0|8,1:0|1:0|1:0,0:0:0:0:
diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs
index 549f0f9214..289f8a00ef 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs
@@ -7,5 +7,20 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
public class ManiaHitWindows : HitWindows
{
+ public override bool IsHitResultAllowed(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.Perfect:
+ case HitResult.Great:
+ case HitResult.Good:
+ case HitResult.Ok:
+ case HitResult.Meh:
+ case HitResult.Miss:
+ return true;
+ }
+
+ return false;
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs
new file mode 100644
index 0000000000..2069329d9a
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs
@@ -0,0 +1,41 @@
+// 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.Input.Bindings;
+
+namespace osu.Game.Rulesets.Mania
+{
+ public class SingleStageVariantGenerator
+ {
+ private readonly int variant;
+ private readonly InputKey[] leftKeys;
+ private readonly InputKey[] rightKeys;
+
+ public SingleStageVariantGenerator(int variant)
+ {
+ this.variant = variant;
+
+ // 10K is special because it expands towards the centre of the keyboard (V/N), rather than towards the edges of the keyboard.
+ if (variant == 10)
+ {
+ leftKeys = new[] { InputKey.A, InputKey.S, InputKey.D, InputKey.F, InputKey.V };
+ rightKeys = new[] { InputKey.N, InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
+ }
+ else
+ {
+ leftKeys = new[] { InputKey.A, InputKey.S, InputKey.D, InputKey.F };
+ rightKeys = new[] { InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
+ }
+ }
+
+ public IEnumerable GenerateMappings() => new VariantMappingGenerator
+ {
+ LeftKeys = leftKeys,
+ RightKeys = rightKeys,
+ SpecialKey = InputKey.Space,
+ SpecialAction = ManiaAction.Special1,
+ NormalActionStart = ManiaAction.Key1,
+ }.GenerateKeyBindingsFor(variant, out _);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
new file mode 100644
index 0000000000..a749f80855
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
@@ -0,0 +1,86 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Animations;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public class LegacyBodyPiece : LegacyManiaColumnElement
+ {
+ private readonly IBindable direction = new Bindable();
+ private readonly IBindable isHitting = new Bindable();
+
+ private Drawable sprite;
+
+ public LegacyBodyPiece()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin, IScrollingInfo scrollingInfo, DrawableHitObject drawableObject)
+ {
+ string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
+ ?? $"mania-note{FallbackColumnIndex}L";
+
+ sprite = skin.GetAnimation(imageName, true, true).With(d =>
+ {
+ if (d == null)
+ return;
+
+ if (d is TextureAnimation animation)
+ animation.IsPlaying = false;
+
+ d.Anchor = Anchor.TopCentre;
+ d.RelativeSizeAxes = Axes.Both;
+ d.Size = Vector2.One;
+ d.FillMode = FillMode.Stretch;
+ // Todo: Wrap
+ });
+
+ if (sprite != null)
+ InternalChild = sprite;
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+
+ var holdNote = (DrawableHoldNote)drawableObject;
+ isHitting.BindTo(holdNote.IsHitting);
+ isHitting.BindValueChanged(onIsHittingChanged, true);
+ }
+
+ private void onIsHittingChanged(ValueChangedEvent isHitting)
+ {
+ if (!(sprite is TextureAnimation animation))
+ return;
+
+ animation.GotoFrame(0);
+ animation.IsPlaying = isHitting.NewValue;
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ if (sprite == null)
+ return;
+
+ if (direction.NewValue == ScrollingDirection.Up)
+ {
+ sprite.Origin = Anchor.BottomCentre;
+ sprite.Scale = new Vector2(1, -1);
+ }
+ else
+ {
+ sprite.Origin = Anchor.TopCentre;
+ sprite.Scale = Vector2.One;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs
new file mode 100644
index 0000000000..64a7641421
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs
@@ -0,0 +1,143 @@
+// 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.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Bindings;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public class LegacyColumnBackground : LegacyManiaColumnElement, IKeyBindingHandler
+ {
+ private readonly IBindable direction = new Bindable();
+ private readonly bool isLastColumn;
+
+ private Container lightContainer;
+ private Sprite light;
+
+ public LegacyColumnBackground(bool isLastColumn)
+ {
+ this.isLastColumn = isLastColumn;
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
+ {
+ string lightImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LightImage)?.Value
+ ?? "mania-stage-light";
+
+ float leftLineWidth = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftLineWidth)
+ ?.Value ?? 1;
+ float rightLineWidth = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.RightLineWidth)
+ ?.Value ?? 1;
+
+ bool hasLeftLine = leftLineWidth > 0;
+ bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m
+ || isLastColumn;
+
+ float lightPosition = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LightPosition)?.Value
+ ?? 0;
+
+ Color4 lineColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLineColour)?.Value
+ ?? Color4.White;
+
+ Color4 backgroundColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour)?.Value
+ ?? Color4.Black;
+
+ Color4 lightColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value
+ ?? Color4.White;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = backgroundColour
+ },
+ new Box
+ {
+ RelativeSizeAxes = Axes.Y,
+ Width = leftLineWidth,
+ Scale = new Vector2(0.740f, 1),
+ Colour = lineColour,
+ Alpha = hasLeftLine ? 1 : 0
+ },
+ new Box
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ RelativeSizeAxes = Axes.Y,
+ Width = rightLineWidth,
+ Scale = new Vector2(0.740f, 1),
+ Colour = lineColour,
+ Alpha = hasRightLine ? 1 : 0
+ },
+ lightContainer = new Container
+ {
+ Origin = Anchor.BottomCentre,
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Bottom = lightPosition },
+ Child = light = new Sprite
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Colour = lightColour,
+ Texture = skin.GetTexture(lightImage),
+ RelativeSizeAxes = Axes.X,
+ Width = 1,
+ Alpha = 0
+ }
+ }
+ };
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ if (direction.NewValue == ScrollingDirection.Up)
+ {
+ lightContainer.Anchor = Anchor.TopCentre;
+ lightContainer.Scale = new Vector2(1, -1);
+ }
+ else
+ {
+ lightContainer.Anchor = Anchor.BottomCentre;
+ lightContainer.Scale = Vector2.One;
+ }
+ }
+
+ public bool OnPressed(ManiaAction action)
+ {
+ if (action == Column.Action.Value)
+ {
+ light.FadeIn();
+ light.ScaleTo(Vector2.One);
+ }
+
+ return false;
+ }
+
+ public void OnReleased(ManiaAction action)
+ {
+ // Todo: Should be 400 * 100 / CurrentBPM
+ const double animation_length = 250;
+
+ if (action == Column.Action.Value)
+ {
+ light.FadeTo(0, animation_length);
+ light.ScaleTo(new Vector2(1, 0), animation_length);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
new file mode 100644
index 0000000000..bc93bb2615
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
@@ -0,0 +1,73 @@
+// 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.Animations;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public class LegacyHitExplosion : LegacyManiaColumnElement
+ {
+ private readonly IBindable direction = new Bindable();
+
+ private Drawable explosion;
+
+ public LegacyHitExplosion()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
+ {
+ string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ExplosionImage)?.Value
+ ?? "lightingN";
+
+ float explosionScale = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ExplosionScale)?.Value
+ ?? 1;
+
+ // Create a temporary animation to retrieve the number of frames, in an effort to calculate the intended frame length.
+ // This animation is discarded and re-queried with the appropriate frame length afterwards.
+ var tmp = skin.GetAnimation(imageName, true, false);
+ double frameLength = 0;
+ if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
+ frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount);
+
+ explosion = skin.GetAnimation(imageName, true, false, frameLength: frameLength).With(d =>
+ {
+ if (d == null)
+ return;
+
+ d.Origin = Anchor.Centre;
+ d.Blending = BlendingParameters.Additive;
+ d.Scale = new Vector2(explosionScale);
+ });
+
+ if (explosion != null)
+ InternalChild = explosion;
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ if (explosion != null)
+ explosion.Anchor = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ explosion?.FadeInFromZero(80)
+ .Then().FadeOut(120);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs
new file mode 100644
index 0000000000..d055ef3480
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs
@@ -0,0 +1,83 @@
+// 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.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public class LegacyHitTarget : CompositeDrawable
+ {
+ private readonly IBindable direction = new Bindable();
+
+ private Container directionContainer;
+
+ public LegacyHitTarget()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
+ {
+ string targetImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitTargetImage)?.Value
+ ?? "mania-stage-hint";
+
+ bool showJudgementLine = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ShowJudgementLine)?.Value
+ ?? true;
+
+ Color4 lineColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.JudgementLineColour)?.Value
+ ?? Color4.White;
+
+ InternalChild = directionContainer = new Container
+ {
+ Origin = Anchor.CentreLeft,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
+ {
+ new Sprite
+ {
+ Texture = skin.GetTexture(targetImage),
+ Scale = new Vector2(1, 0.9f * 1.6025f),
+ RelativeSizeAxes = Axes.X,
+ Width = 1
+ },
+ new Box
+ {
+ Anchor = Anchor.CentreLeft,
+ RelativeSizeAxes = Axes.X,
+ Height = 1,
+ Colour = lineColour,
+ Alpha = showJudgementLine ? 0.9f : 0
+ }
+ }
+ };
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ if (direction.NewValue == ScrollingDirection.Up)
+ {
+ directionContainer.Anchor = Anchor.TopLeft;
+ directionContainer.Scale = new Vector2(1, -1);
+ }
+ else
+ {
+ directionContainer.Anchor = Anchor.BottomLeft;
+ directionContainer.Scale = Vector2.One;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteHeadPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteHeadPiece.cs
new file mode 100644
index 0000000000..c5aa062d0f
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteHeadPiece.cs
@@ -0,0 +1,18 @@
+// 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.Textures;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public class LegacyHoldNoteHeadPiece : LegacyNotePiece
+ {
+ protected override Texture GetTexture(ISkinSource skin)
+ {
+ // TODO: Should fallback to the head from default legacy skin instead of note.
+ return GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage)
+ ?? GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs
new file mode 100644
index 0000000000..2e8259f10a
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs
@@ -0,0 +1,29 @@
+// 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.Graphics.Textures;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public class LegacyHoldNoteTailPiece : LegacyNotePiece
+ {
+ protected override void OnDirectionChanged(ValueChangedEvent direction)
+ {
+ // Invert the direction
+ base.OnDirectionChanged(direction.NewValue == ScrollingDirection.Up
+ ? new ValueChangedEvent(ScrollingDirection.Down, ScrollingDirection.Down)
+ : new ValueChangedEvent(ScrollingDirection.Up, ScrollingDirection.Up));
+ }
+
+ protected override Texture GetTexture(ISkinSource skin)
+ {
+ // TODO: Should fallback to the head from default legacy skin instead of note.
+ return GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteTailImage)
+ ?? GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage)
+ ?? GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs
new file mode 100644
index 0000000000..44f3e7d7b3
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs
@@ -0,0 +1,106 @@
+// 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.Framework.Input.Bindings;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public class LegacyKeyArea : LegacyManiaColumnElement, IKeyBindingHandler
+ {
+ private readonly IBindable direction = new Bindable();
+
+ private Container directionContainer;
+ private Sprite upSprite;
+ private Sprite downSprite;
+
+ [Resolved]
+ private Column column { get; set; }
+
+ public LegacyKeyArea()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
+ {
+ string upImage = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeyImage)?.Value
+ ?? $"mania-key{FallbackColumnIndex}";
+
+ string downImage = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeyImageDown)?.Value
+ ?? $"mania-key{FallbackColumnIndex}D";
+
+ InternalChild = directionContainer = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
+ {
+ upSprite = new Sprite
+ {
+ Origin = Anchor.BottomCentre,
+ Texture = skin.GetTexture(upImage),
+ RelativeSizeAxes = Axes.X,
+ Width = 1
+ },
+ downSprite = new Sprite
+ {
+ Origin = Anchor.BottomCentre,
+ Texture = skin.GetTexture(downImage),
+ RelativeSizeAxes = Axes.X,
+ Width = 1,
+ Alpha = 0
+ }
+ }
+ };
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ if (direction.NewValue == ScrollingDirection.Up)
+ {
+ directionContainer.Anchor = directionContainer.Origin = Anchor.TopCentre;
+ upSprite.Anchor = downSprite.Anchor = Anchor.TopCentre;
+ upSprite.Scale = downSprite.Scale = new Vector2(1, -1);
+ }
+ else
+ {
+ directionContainer.Anchor = directionContainer.Origin = Anchor.BottomCentre;
+ upSprite.Anchor = downSprite.Anchor = Anchor.BottomCentre;
+ upSprite.Scale = downSprite.Scale = Vector2.One;
+ }
+ }
+
+ public bool OnPressed(ManiaAction action)
+ {
+ if (action == column.Action.Value)
+ {
+ upSprite.FadeTo(0);
+ downSprite.FadeTo(1);
+ }
+
+ return false;
+ }
+
+ public void OnReleased(ManiaAction action)
+ {
+ if (action == column.Action.Value)
+ {
+ upSprite.FadeTo(1);
+ downSprite.FadeTo(0);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs
new file mode 100644
index 0000000000..3c0c632c14
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs
@@ -0,0 +1,48 @@
+// 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.Containers;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ ///
+ /// A which is placed somewhere within a .
+ ///
+ public class LegacyManiaColumnElement : CompositeDrawable
+ {
+ [Resolved]
+ protected Column Column { get; private set; }
+
+ ///
+ /// The column type identifier to use for texture lookups, in the case of no user-provided configuration.
+ ///
+ protected string FallbackColumnIndex { get; private set; }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ switch (Column.ColumnType)
+ {
+ case ColumnType.Special:
+ FallbackColumnIndex = "S";
+ break;
+
+ case ColumnType.Odd:
+ FallbackColumnIndex = "1";
+ break;
+
+ case ColumnType.Even:
+ FallbackColumnIndex = "2";
+ break;
+ }
+ }
+
+ protected IBindable GetColumnSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup)
+ => skin.GetManiaSkinConfig(lookup, Column.Index);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs
new file mode 100644
index 0000000000..515c941d65
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs
@@ -0,0 +1,98 @@
+// 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.Framework.Graphics.Textures;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public class LegacyNotePiece : LegacyManiaColumnElement
+ {
+ private readonly IBindable direction = new Bindable();
+
+ private Container directionContainer;
+ private Sprite noteSprite;
+
+ private float? minimumColumnWidth;
+
+ public LegacyNotePiece()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
+ {
+ minimumColumnWidth = skin.GetConfig(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.MinimumColumnWidth))?.Value;
+
+ InternalChild = directionContainer = new Container
+ {
+ Origin = Anchor.BottomCentre,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Child = noteSprite = new Sprite { Texture = GetTexture(skin) }
+ };
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(OnDirectionChanged, true);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (noteSprite.Texture != null)
+ {
+ // The height is scaled to the minimum column width, if provided.
+ float minimumWidth = minimumColumnWidth ?? DrawWidth;
+
+ noteSprite.Scale = Vector2.Divide(new Vector2(DrawWidth, minimumWidth), noteSprite.Texture.DisplayWidth);
+ }
+ }
+
+ protected virtual void OnDirectionChanged(ValueChangedEvent direction)
+ {
+ if (direction.NewValue == ScrollingDirection.Up)
+ {
+ directionContainer.Anchor = Anchor.TopCentre;
+ directionContainer.Scale = new Vector2(1, -1);
+ }
+ else
+ {
+ directionContainer.Anchor = Anchor.BottomCentre;
+ directionContainer.Scale = Vector2.One;
+ }
+ }
+
+ protected virtual Texture GetTexture(ISkinSource skin) => GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage);
+
+ protected Texture GetTextureFromLookup(ISkin skin, LegacyManiaSkinConfigurationLookups lookup)
+ {
+ string suffix = string.Empty;
+
+ switch (lookup)
+ {
+ case LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage:
+ suffix = "H";
+ break;
+
+ case LegacyManiaSkinConfigurationLookups.HoldNoteTailImage:
+ suffix = "T";
+ break;
+ }
+
+ string noteImage = GetColumnSkinConfig(skin, lookup)?.Value
+ ?? $"mania-note{FallbackColumnIndex}{suffix}";
+
+ return skin.GetTexture(noteImage);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs
new file mode 100644
index 0000000000..7f5de601ca
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs
@@ -0,0 +1,62 @@
+// 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.Framework.Graphics.Sprites;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public class LegacyStageBackground : CompositeDrawable
+ {
+ private Drawable leftSprite;
+ private Drawable rightSprite;
+
+ public LegacyStageBackground()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ string leftImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LeftStageImage)?.Value
+ ?? "mania-stage-left";
+
+ string rightImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.RightStageImage)?.Value
+ ?? "mania-stage-right";
+
+ InternalChildren = new[]
+ {
+ leftSprite = new Sprite
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopRight,
+ X = 0.05f,
+ Texture = skin.GetTexture(leftImage),
+ },
+ rightSprite = new Sprite
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopLeft,
+ X = -0.05f,
+ Texture = skin.GetTexture(rightImage)
+ }
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (leftSprite?.Height > 0)
+ leftSprite.Scale = new Vector2(1, DrawHeight / leftSprite.Height);
+
+ if (rightSprite?.Height > 0)
+ rightSprite.Scale = new Vector2(1, DrawHeight / rightSprite.Height);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs
new file mode 100644
index 0000000000..4609fcc849
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.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 osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public class LegacyStageForeground : CompositeDrawable
+ {
+ private readonly IBindable direction = new Bindable();
+
+ private Drawable sprite;
+
+ public LegacyStageForeground()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
+ {
+ string bottomImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.BottomStageImage)?.Value
+ ?? "mania-stage-bottom";
+
+ sprite = skin.GetAnimation(bottomImage, true, true)?.With(d =>
+ {
+ if (d == null)
+ return;
+
+ d.Scale = new Vector2(1.6f);
+ });
+
+ if (sprite != null)
+ InternalChild = sprite;
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ if (sprite == null)
+ return;
+
+ if (direction.NewValue == ScrollingDirection.Up)
+ sprite.Anchor = sprite.Origin = Anchor.TopCentre;
+ else
+ sprite.Anchor = sprite.Origin = Anchor.BottomCentre;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
index f3739ce7c2..e167135556 100644
--- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
@@ -1,67 +1,152 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Textures;
-using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Audio;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Skinning;
+using System.Collections.Generic;
+using osu.Framework.Audio.Sample;
+using osu.Game.Audio;
+using osu.Game.Rulesets.Objects.Legacy;
namespace osu.Game.Rulesets.Mania.Skinning
{
- public class ManiaLegacySkinTransformer : ISkin
+ public class ManiaLegacySkinTransformer : LegacySkinTransformer
{
- private readonly ISkin source;
+ private readonly ManiaBeatmap beatmap;
- public ManiaLegacySkinTransformer(ISkin source)
+ ///
+ /// Mapping of to their corresponding
+ /// value.
+ ///
+ private static readonly IReadOnlyDictionary hitresult_mapping
+ = new Dictionary
+ {
+ { HitResult.Perfect, LegacyManiaSkinConfigurationLookups.Hit300g },
+ { HitResult.Great, LegacyManiaSkinConfigurationLookups.Hit300 },
+ { HitResult.Good, LegacyManiaSkinConfigurationLookups.Hit200 },
+ { HitResult.Ok, LegacyManiaSkinConfigurationLookups.Hit100 },
+ { HitResult.Meh, LegacyManiaSkinConfigurationLookups.Hit50 },
+ { HitResult.Miss, LegacyManiaSkinConfigurationLookups.Hit0 }
+ };
+
+ ///
+ /// Mapping of to their corresponding
+ /// default filenames.
+ ///
+ private static readonly IReadOnlyDictionary default_hitresult_skin_filenames
+ = new Dictionary
+ {
+ { HitResult.Perfect, "mania-hit300g" },
+ { HitResult.Great, "mania-hit300" },
+ { HitResult.Good, "mania-hit200" },
+ { HitResult.Ok, "mania-hit100" },
+ { HitResult.Meh, "mania-hit50" },
+ { HitResult.Miss, "mania-hit0" }
+ };
+
+ private Lazy isLegacySkin;
+
+ ///
+ /// Whether texture for the keys exists.
+ /// Used to determine if the mania ruleset is skinned.
+ ///
+ private Lazy hasKeyTexture;
+
+ public ManiaLegacySkinTransformer(ISkinSource source, IBeatmap beatmap)
+ : base(source)
{
- this.source = source;
+ this.beatmap = (ManiaBeatmap)beatmap;
+
+ Source.SourceChanged += sourceChanged;
+ sourceChanged();
}
- public Drawable GetDrawableComponent(ISkinComponent component)
+ private void sourceChanged()
+ {
+ isLegacySkin = new Lazy(() => Source.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null);
+ hasKeyTexture = new Lazy(() => Source.GetAnimation(
+ this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value
+ ?? "mania-key1", true, true) != null);
+ }
+
+ public override Drawable GetDrawableComponent(ISkinComponent component)
{
switch (component)
{
case GameplaySkinComponent resultComponent:
- return getResult(resultComponent);
+ return getResult(resultComponent.Component);
+
+ case ManiaSkinComponent maniaComponent:
+ if (!isLegacySkin.Value || !hasKeyTexture.Value)
+ return null;
+
+ switch (maniaComponent.Component)
+ {
+ case ManiaSkinComponents.ColumnBackground:
+ return new LegacyColumnBackground(maniaComponent.TargetColumn == beatmap.TotalColumns - 1);
+
+ case ManiaSkinComponents.HitTarget:
+ return new LegacyHitTarget();
+
+ case ManiaSkinComponents.KeyArea:
+ return new LegacyKeyArea();
+
+ case ManiaSkinComponents.Note:
+ return new LegacyNotePiece();
+
+ case ManiaSkinComponents.HoldNoteHead:
+ return new LegacyHoldNoteHeadPiece();
+
+ case ManiaSkinComponents.HoldNoteTail:
+ return new LegacyHoldNoteTailPiece();
+
+ case ManiaSkinComponents.HoldNoteBody:
+ return new LegacyBodyPiece();
+
+ case ManiaSkinComponents.HitExplosion:
+ return new LegacyHitExplosion();
+
+ case ManiaSkinComponents.StageBackground:
+ return new LegacyStageBackground();
+
+ case ManiaSkinComponents.StageForeground:
+ return new LegacyStageForeground();
+ }
+
+ break;
}
return null;
}
- private Drawable getResult(GameplaySkinComponent resultComponent)
+ private Drawable getResult(HitResult result)
{
- switch (resultComponent.Component)
- {
- case HitResult.Miss:
- return this.GetAnimation("mania-hit0", true, true);
+ string filename = this.GetManiaSkinConfig(hitresult_mapping[result])?.Value
+ ?? default_hitresult_skin_filenames[result];
- case HitResult.Meh:
- return this.GetAnimation("mania-hit50", true, true);
-
- case HitResult.Ok:
- return this.GetAnimation("mania-hit100", true, true);
-
- case HitResult.Good:
- return this.GetAnimation("mania-hit200", true, true);
-
- case HitResult.Great:
- return this.GetAnimation("mania-hit300", true, true);
-
- case HitResult.Perfect:
- return this.GetAnimation("mania-hit300g", true, true);
- }
-
- return null;
+ return this.GetAnimation(filename, true, true);
}
- public Texture GetTexture(string componentName) => source.GetTexture(componentName);
+ public override SampleChannel GetSample(ISampleInfo sampleInfo)
+ {
+ // layered hit sounds never play in mania
+ if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
+ return new SampleChannelVirtual();
- public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample);
+ return Source.GetSample(sampleInfo);
+ }
- public IBindable GetConfig(TLookup lookup) =>
- source.GetConfig(lookup);
+ public override IBindable GetConfig(TLookup lookup)
+ {
+ if (lookup is ManiaSkinConfigurationLookup maniaLookup)
+ return Source.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn));
+
+ return Source.GetConfig(lookup);
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs
new file mode 100644
index 0000000000..2e17a6bef1
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.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.Framework.Bindables;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public static class ManiaSkinConfigExtensions
+ {
+ ///
+ /// Retrieve a per-column-count skin configuration.
+ ///
+ /// The skin from which configuration is retrieved.
+ /// The value to retrieve.
+ /// If not null, denotes the index of the column to which the entry applies.
+ public static IBindable GetManiaSkinConfig(this ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null)
+ => skin.GetConfig(
+ new ManiaSkinConfigurationLookup(lookup, index));
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs
new file mode 100644
index 0000000000..f07a5518b7
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs
@@ -0,0 +1,33 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public class ManiaSkinConfigurationLookup
+ {
+ ///
+ /// The configuration lookup value.
+ ///
+ public readonly LegacyManiaSkinConfigurationLookups Lookup;
+
+ ///
+ /// The intended index for the configuration.
+ /// May be null if the configuration does not apply to a .
+ ///
+ public readonly int? TargetColumn;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The lookup value.
+ /// The intended index for the configuration. May be null if the configuration does not apply to a .
+ public ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups lookup, int? targetColumn = null)
+ {
+ Lookup = lookup;
+ TargetColumn = targetColumn;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 63c573d344..511d6c8623 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -12,17 +12,19 @@ using osu.Framework.Bindables;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Skinning;
using osuTK;
+using osu.Game.Rulesets.Mania.Beatmaps;
namespace osu.Game.Rulesets.Mania.UI
{
+ [Cached]
public class Column : ScrollingPlayfield, IKeyBindingHandler, IHasAccentColour
{
public const float COLUMN_WIDTH = 80;
- private const float special_column_width = 70;
+ public const float SPECIAL_COLUMN_WIDTH = 70;
///
/// The index of this column as part of the whole playfield.
@@ -31,12 +33,11 @@ namespace osu.Game.Rulesets.Mania.UI
public readonly Bindable Action = new Bindable();
- private readonly ColumnBackground background;
- private readonly ColumnKeyArea keyArea;
private readonly ColumnHitObjectArea hitObjectArea;
internal readonly Container TopLevelContainer;
- private readonly Container explosionContainer;
+
+ public Container UnderlayElements => hitObjectArea.UnderlayElements;
public Column(int index)
{
@@ -45,95 +46,34 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y;
Width = COLUMN_WIDTH;
- background = new ColumnBackground { RelativeSizeAxes = Axes.Both };
-
- Container hitTargetContainer;
+ Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, Index), _ => new DefaultColumnBackground())
+ {
+ RelativeSizeAxes = Axes.Both
+ };
InternalChildren = new[]
{
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(),
- hitTargetContainer = new Container
+ hitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
+ new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, Index), _ => new DefaultKeyArea())
{
- Name = "Hit target + hit objects",
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- hitObjectArea = new ColumnHitObjectArea(HitObjectContainer)
- {
- RelativeSizeAxes = Axes.Both,
- },
- explosionContainer = new Container
- {
- Name = "Hit explosions",
- RelativeSizeAxes = Axes.Both,
- }
- }
- },
- keyArea = new ColumnKeyArea
- {
- RelativeSizeAxes = Axes.X,
- Height = ManiaStage.HIT_TARGET_POSITION,
+ RelativeSizeAxes = Axes.Both
},
background,
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
};
- TopLevelContainer.Add(explosionContainer.CreateProxy());
-
- Direction.BindValueChanged(dir =>
- {
- hitTargetContainer.Padding = new MarginPadding
- {
- Top = dir.NewValue == ScrollingDirection.Up ? ManiaStage.HIT_TARGET_POSITION : 0,
- Bottom = dir.NewValue == ScrollingDirection.Down ? ManiaStage.HIT_TARGET_POSITION : 0,
- };
-
- explosionContainer.Padding = new MarginPadding
- {
- Top = dir.NewValue == ScrollingDirection.Up ? NotePiece.NOTE_HEIGHT / 2 : 0,
- Bottom = dir.NewValue == ScrollingDirection.Down ? NotePiece.NOTE_HEIGHT / 2 : 0
- };
-
- keyArea.Anchor = keyArea.Origin = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
- }, true);
+ TopLevelContainer.Add(hitObjectArea.Explosions.CreateProxy());
}
public override Axes RelativeSizeAxes => Axes.Y;
- private bool isSpecial;
+ public ColumnType ColumnType { get; set; }
- public bool IsSpecial
- {
- get => isSpecial;
- set
- {
- if (isSpecial == value)
- return;
+ public bool IsSpecial => ColumnType == ColumnType.Special;
- isSpecial = value;
-
- Width = isSpecial ? special_column_width : COLUMN_WIDTH;
- }
- }
-
- private Color4 accentColour;
-
- public Color4 AccentColour
- {
- get => accentColour;
- set
- {
- if (accentColour == value)
- return;
-
- accentColour = value;
-
- background.AccentColour = value;
- keyArea.AccentColour = value;
- hitObjectArea.AccentColour = value;
- }
- }
+ public Color4 AccentColour { get; set; }
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
@@ -168,11 +108,15 @@ namespace osu.Game.Rulesets.Mania.UI
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
- explosionContainer.Add(new HitExplosion(judgedObject.AccentColour.Value, judgedObject is DrawableHoldNoteTick)
+ var explosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, Index), _ =>
+ new DefaultHitExplosion(judgedObject.AccentColour.Value, judgedObject is DrawableHoldNoteTick))
{
- Anchor = Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre,
- Origin = Anchor.Centre
- });
+ RelativeSizeAxes = Axes.Both
+ };
+
+ hitObjectArea.Explosions.Add(explosion);
+
+ explosion.Delay(200).Expire(true);
}
public bool OnPressed(ManiaAction action)
@@ -197,6 +141,6 @@ namespace osu.Game.Rulesets.Mania.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
- => DrawRectangle.Inflate(new Vector2(ManiaStage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
+ => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
index 90e78c3899..b365ae45a9 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
@@ -1,145 +1,53 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Effects;
-using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics;
-using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
-using osuTK.Graphics;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.UI.Components
{
- public class ColumnHitObjectArea : CompositeDrawable, IHasAccentColour
+ public class ColumnHitObjectArea : HitObjectArea
{
- private readonly IBindable direction = new Bindable();
+ public readonly Container Explosions;
+
+ public readonly Container UnderlayElements;
private readonly Drawable hitTarget;
- public ColumnHitObjectArea(HitObjectContainer hitObjectContainer)
+ public ColumnHitObjectArea(int columnIndex, HitObjectContainer hitObjectContainer)
+ : base(hitObjectContainer)
{
- InternalChildren = new[]
+ AddRangeInternal(new[]
{
- hitTarget = new DefaultHitTarget
+ UnderlayElements = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Depth = 2,
+ },
+ hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget, columnIndex), _ => new DefaultHitTarget())
{
RelativeSizeAxes = Axes.X,
+ Depth = 1
},
- hitObjectContainer
- };
- }
-
- [BackgroundDependencyLoader]
- private void load(IScrollingInfo scrollingInfo)
- {
- direction.BindTo(scrollingInfo.Direction);
- direction.BindValueChanged(dir =>
- {
- Anchor anchor = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
-
- hitTarget.Anchor = hitTarget.Origin = anchor;
- }, true);
- }
-
- private Color4 accentColour;
-
- public Color4 AccentColour
- {
- get => accentColour;
- set
- {
- if (accentColour == value)
- return;
-
- accentColour = value;
-
- if (hitTarget is IHasAccentColour colouredHitTarget)
- colouredHitTarget.AccentColour = accentColour;
- }
- }
-
- private class DefaultHitTarget : CompositeDrawable, IHasAccentColour
- {
- private const float hit_target_bar_height = 2;
-
- private readonly IBindable direction = new Bindable();
-
- private readonly Container hitTargetLine;
- private readonly Drawable hitTargetBar;
-
- public DefaultHitTarget()
- {
- InternalChildren = new[]
+ Explosions = new Container
{
- hitTargetBar = new Box
- {
- RelativeSizeAxes = Axes.X,
- Height = NotePiece.NOTE_HEIGHT,
- Alpha = 0.6f,
- Colour = Color4.Black
- },
- hitTargetLine = new Container
- {
- RelativeSizeAxes = Axes.X,
- Height = hit_target_bar_height,
- Masking = true,
- Child = new Box { RelativeSizeAxes = Axes.Both }
- },
- };
- }
-
- [BackgroundDependencyLoader]
- private void load(IScrollingInfo scrollingInfo)
- {
- direction.BindTo(scrollingInfo.Direction);
- direction.BindValueChanged(dir =>
- {
- Anchor anchor = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
-
- hitTargetBar.Anchor = hitTargetBar.Origin = anchor;
- hitTargetLine.Anchor = hitTargetLine.Origin = anchor;
- }, true);
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
- updateColours();
- }
-
- private Color4 accentColour;
-
- public Color4 AccentColour
- {
- get => accentColour;
- set
- {
- if (accentColour == value)
- return;
-
- accentColour = value;
-
- updateColours();
+ RelativeSizeAxes = Axes.Both,
+ Depth = -1,
}
- }
+ });
+ }
- private void updateColours()
- {
- if (!IsLoaded)
- return;
+ protected override void UpdateHitPosition()
+ {
+ base.UpdateHitPosition();
- hitTargetLine.EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Radius = 5,
- Colour = accentColour.Opacity(0.5f),
- };
- }
+ if (Direction.Value == ScrollingDirection.Up)
+ hitTarget.Anchor = hitTarget.Origin = Anchor.TopLeft;
+ else
+ hitTarget.Anchor = hitTarget.Origin = Anchor.BottomLeft;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnKeyArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnKeyArea.cs
deleted file mode 100644
index 60fc2713b3..0000000000
--- a/osu.Game.Rulesets.Mania/UI/Components/ColumnKeyArea.cs
+++ /dev/null
@@ -1,124 +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.Bindables;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Colour;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Effects;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Input.Bindings;
-using osu.Game.Graphics;
-using osu.Game.Rulesets.UI.Scrolling;
-using osuTK;
-using osuTK.Graphics;
-
-namespace osu.Game.Rulesets.Mania.UI.Components
-{
- public class ColumnKeyArea : CompositeDrawable, IKeyBindingHandler, IHasAccentColour
- {
- private const float key_icon_size = 10;
- private const float key_icon_corner_radius = 3;
-
- private readonly IBindable action = new Bindable();
- private readonly IBindable direction = new Bindable();
-
- private Container keyIcon;
-
- [BackgroundDependencyLoader]
- private void load(IBindable action, IScrollingInfo scrollingInfo)
- {
- this.action.BindTo(action);
-
- Drawable gradient;
-
- InternalChildren = new[]
- {
- gradient = new Box
- {
- Name = "Key gradient",
- RelativeSizeAxes = Axes.Both,
- Alpha = 0.5f
- },
- keyIcon = new Container
- {
- Name = "Key icon",
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(key_icon_size),
- Masking = true,
- CornerRadius = key_icon_corner_radius,
- BorderThickness = 2,
- BorderColour = Color4.White, // Not true
- Children = new[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- AlwaysPresent = true
- }
- }
- }
- };
-
- direction.BindTo(scrollingInfo.Direction);
- direction.BindValueChanged(dir =>
- {
- gradient.Colour = ColourInfo.GradientVertical(
- dir.NewValue == ScrollingDirection.Up ? Color4.Black : Color4.Black.Opacity(0),
- dir.NewValue == ScrollingDirection.Up ? Color4.Black.Opacity(0) : Color4.Black);
- }, true);
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
- updateColours();
- }
-
- private Color4 accentColour;
-
- public Color4 AccentColour
- {
- get => accentColour;
- set
- {
- if (accentColour == value)
- return;
-
- accentColour = value;
-
- updateColours();
- }
- }
-
- private void updateColours()
- {
- if (!IsLoaded)
- return;
-
- keyIcon.EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Radius = 5,
- Colour = accentColour.Opacity(0.5f),
- };
- }
-
- public bool OnPressed(ManiaAction action)
- {
- if (action == this.action.Value)
- keyIcon.ScaleTo(1.4f, 50, Easing.OutQuint).Then().ScaleTo(1.3f, 250, Easing.OutQuint);
- return false;
- }
-
- public void OnReleased(ManiaAction action)
- {
- if (action == this.action.Value)
- keyIcon.ScaleTo(1f, 125, Easing.OutQuint);
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs
new file mode 100644
index 0000000000..4b4bc157d5
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs
@@ -0,0 +1,90 @@
+// 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.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Bindings;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.UI.Components
+{
+ public class DefaultColumnBackground : CompositeDrawable, IKeyBindingHandler
+ {
+ private readonly IBindable direction = new Bindable();
+
+ private Color4 brightColour;
+ private Color4 dimColour;
+
+ private Box background;
+ private Box backgroundOverlay;
+
+ [Resolved]
+ private Column column { get; set; }
+
+ public DefaultColumnBackground()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
+ InternalChildren = new[]
+ {
+ background = new Box
+ {
+ Name = "Background",
+ RelativeSizeAxes = Axes.Both,
+ },
+ backgroundOverlay = new Box
+ {
+ Name = "Background Gradient Overlay",
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.5f,
+ Blending = BlendingParameters.Additive,
+ Alpha = 0
+ }
+ };
+
+ background.Colour = column.AccentColour.Darken(5);
+ brightColour = column.AccentColour.Opacity(0.6f);
+ dimColour = column.AccentColour.Opacity(0);
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ if (direction.NewValue == ScrollingDirection.Up)
+ {
+ backgroundOverlay.Anchor = backgroundOverlay.Origin = Anchor.TopLeft;
+ backgroundOverlay.Colour = ColourInfo.GradientVertical(brightColour, dimColour);
+ }
+ else
+ {
+ backgroundOverlay.Anchor = backgroundOverlay.Origin = Anchor.BottomLeft;
+ backgroundOverlay.Colour = ColourInfo.GradientVertical(dimColour, brightColour);
+ }
+ }
+
+ public bool OnPressed(ManiaAction action)
+ {
+ if (action == column.Action.Value)
+ backgroundOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint);
+ return false;
+ }
+
+ public void OnReleased(ManiaAction action)
+ {
+ if (action == column.Action.Value)
+ backgroundOverlay.FadeTo(0, 250, Easing.OutQuint);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs
new file mode 100644
index 0000000000..e0b099ab9b
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs
@@ -0,0 +1,80 @@
+// 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.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.UI.Components
+{
+ public class DefaultHitTarget : CompositeDrawable
+ {
+ private const float hit_target_bar_height = 2;
+
+ private readonly IBindable direction = new Bindable();
+
+ private Container hitTargetLine;
+ private Drawable hitTargetBar;
+
+ [Resolved]
+ private Column column { get; set; }
+
+ public DefaultHitTarget()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
+ InternalChildren = new[]
+ {
+ hitTargetBar = new Box
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = DefaultNotePiece.NOTE_HEIGHT,
+ Alpha = 0.6f,
+ Colour = Color4.Black
+ },
+ hitTargetLine = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = hit_target_bar_height,
+ Masking = true,
+ Child = new Box { RelativeSizeAxes = Axes.Both }
+ },
+ };
+
+ hitTargetLine.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Radius = 5,
+ Colour = column.AccentColour.Opacity(0.5f),
+ };
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ if (direction.NewValue == ScrollingDirection.Up)
+ {
+ hitTargetBar.Anchor = hitTargetBar.Origin = Anchor.TopLeft;
+ hitTargetLine.Anchor = hitTargetLine.Origin = Anchor.TopLeft;
+ }
+ else
+ {
+ hitTargetBar.Anchor = hitTargetBar.Origin = Anchor.BottomLeft;
+ hitTargetLine.Anchor = hitTargetLine.Origin = Anchor.BottomLeft;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs
new file mode 100644
index 0000000000..47cb9bd45a
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.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 osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Bindings;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.UI.Components
+{
+ public class DefaultKeyArea : CompositeDrawable, IKeyBindingHandler
+ {
+ private const float key_icon_size = 10;
+ private const float key_icon_corner_radius = 3;
+
+ private readonly IBindable direction = new Bindable();
+
+ private Container directionContainer;
+ private Container keyIcon;
+ private Drawable gradient;
+
+ [Resolved]
+ private Column column { get; set; }
+
+ public DefaultKeyArea()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
+ InternalChild = directionContainer = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = Stage.HIT_TARGET_POSITION,
+ Children = new[]
+ {
+ gradient = new Box
+ {
+ Name = "Key gradient",
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.5f
+ },
+ keyIcon = new Container
+ {
+ Name = "Key icon",
+ Size = new Vector2(key_icon_size),
+ Origin = Anchor.Centre,
+ Masking = true,
+ CornerRadius = key_icon_corner_radius,
+ BorderThickness = 2,
+ BorderColour = Color4.White, // Not true
+ Children = new[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true
+ }
+ }
+ }
+ }
+ };
+
+ keyIcon.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Radius = 5,
+ Colour = column.AccentColour.Opacity(0.5f),
+ };
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ if (direction.NewValue == ScrollingDirection.Up)
+ {
+ keyIcon.Anchor = Anchor.BottomCentre;
+ keyIcon.Y = -20;
+ directionContainer.Anchor = directionContainer.Origin = Anchor.TopLeft;
+ gradient.Colour = ColourInfo.GradientVertical(Color4.Black, Color4.Black.Opacity(0));
+ }
+ else
+ {
+ keyIcon.Anchor = Anchor.TopCentre;
+ keyIcon.Y = 20;
+ directionContainer.Anchor = directionContainer.Origin = Anchor.BottomLeft;
+ gradient.Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0), Color4.Black);
+ }
+ }
+
+ public bool OnPressed(ManiaAction action)
+ {
+ if (action == column.Action.Value)
+ keyIcon.ScaleTo(1.4f, 50, Easing.OutQuint).Then().ScaleTo(1.3f, 250, Easing.OutQuint);
+ return false;
+ }
+
+ public void OnReleased(ManiaAction action)
+ {
+ if (action == column.Action.Value)
+ keyIcon.ScaleTo(1f, 125, Easing.OutQuint);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs
new file mode 100644
index 0000000000..f5b542d085
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs
@@ -0,0 +1,30 @@
+// 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.Framework.Graphics.Shapes;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.UI.Components
+{
+ public class DefaultStageBackground : CompositeDrawable
+ {
+ public DefaultStageBackground()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = new Box
+ {
+ Name = "Background",
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs
new file mode 100644
index 0000000000..ba5281a1a2
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs
@@ -0,0 +1,55 @@
+// 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.Game.Rulesets.Mania.Skinning;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.UI.Components
+{
+ public class HitObjectArea : SkinReloadableDrawable
+ {
+ protected readonly IBindable Direction = new Bindable();
+
+ public HitObjectArea(HitObjectContainer hitObjectContainer)
+ {
+ InternalChildren = new[]
+ {
+ hitObjectContainer,
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
+ Direction.BindTo(scrollingInfo.Direction);
+ Direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ protected override void SkinChanged(ISkinSource skin, bool allowFallback)
+ {
+ base.SkinChanged(skin, allowFallback);
+ UpdateHitPosition();
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ UpdateHitPosition();
+ }
+
+ protected virtual void UpdateHitPosition()
+ {
+ float hitPosition = CurrentSkin.GetConfig(
+ new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
+ ?? Stage.HIT_TARGET_POSITION;
+
+ Padding = Direction.Value == ScrollingDirection.Up
+ ? new MarginPadding { Top = hitPosition }
+ : new MarginPadding { Bottom = hitPosition };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
new file mode 100644
index 0000000000..7a047ed121
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
@@ -0,0 +1,155 @@
+// 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.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.UI
+{
+ public class DefaultHitExplosion : CompositeDrawable
+ {
+ public override bool RemoveWhenNotAlive => true;
+
+ private readonly IBindable direction = new Bindable();
+
+ private readonly CircularContainer largeFaint;
+ private readonly CircularContainer mainGlow1;
+
+ public DefaultHitExplosion(Color4 objectColour, bool isSmall = false)
+ {
+ Origin = Anchor.Centre;
+
+ RelativeSizeAxes = Axes.X;
+ Height = DefaultNotePiece.NOTE_HEIGHT;
+
+ // 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 initial_height = 10;
+
+ var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1);
+
+ InternalChildren = new Drawable[]
+ {
+ largeFaint = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ // we want our size to be very small so the glow dominates it.
+ Size = new Vector2(0.8f),
+ Blending = BlendingParameters.Additive,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
+ Roundness = 160,
+ Radius = 200,
+ },
+ },
+ mainGlow1 = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Blending = BlendingParameters.Additive,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
+ Roundness = 20,
+ Radius = 50,
+ },
+ },
+ new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Size = new Vector2(0.01f, initial_height),
+ Blending = BlendingParameters.Additive,
+ Rotation = RNG.NextSingle(-angle_variangle, angle_variangle),
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = colour,
+ Roundness = roundness,
+ Radius = 40,
+ },
+ },
+ new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Size = new Vector2(0.01f, initial_height),
+ Blending = BlendingParameters.Additive,
+ Rotation = RNG.NextSingle(-angle_variangle, angle_variangle),
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = colour,
+ Roundness = roundness,
+ Radius = 40,
+ },
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ protected override void LoadComplete()
+ {
+ const double duration = 200;
+
+ base.LoadComplete();
+
+ 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);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ if (direction.NewValue == ScrollingDirection.Up)
+ {
+ Anchor = Anchor.TopCentre;
+ Y = DefaultNotePiece.NOTE_HEIGHT / 2;
+ }
+ else
+ {
+ Anchor = Anchor.BottomCentre;
+ Y = -DefaultNotePiece.NOTE_HEIGHT / 2;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index 2c497541a8..94b5ee9486 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -2,11 +2,15 @@
// 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.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Handlers;
using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps;
@@ -19,12 +23,21 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
-using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
public class DrawableManiaRuleset : DrawableScrollingRuleset
{
+ ///
+ /// The minimum time range. This occurs at a of 40.
+ ///
+ public const double MIN_TIME_RANGE = 150;
+
+ ///
+ /// The maximum time range. This occurs at a of 1.
+ ///
+ public const double MAX_TIME_RANGE = 6000;
+
protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap;
@@ -36,6 +49,10 @@ namespace osu.Game.Rulesets.Mania.UI
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
private readonly Bindable configDirection = new Bindable();
+ private readonly Bindable configTimeRange = new BindableDouble();
+
+ // Stores the current speed adjustment active in gameplay.
+ private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
: base(ruleset, beatmap, mods)
@@ -46,20 +63,49 @@ namespace osu.Game.Rulesets.Mania.UI
[BackgroundDependencyLoader]
private void load()
{
+ foreach (var mod in Mods.OfType())
+ mod.ApplyToTrack(speedAdjustmentTrack);
+
+ bool isForCurrentRuleset = Beatmap.BeatmapInfo.Ruleset.Equals(Ruleset.RulesetInfo);
+
+ foreach (var p in ControlPoints)
+ {
+ // Mania doesn't care about global velocity
+ p.Velocity = 1;
+ p.BaseBeatLength *= Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier;
+
+ // For non-mania beatmap, speed changes should only happen through timing points
+ if (!isForCurrentRuleset)
+ p.DifficultyPoint = new DifficultyControlPoint();
+ }
+
BarLines.ForEach(Playfield.Add);
Config.BindWith(ManiaRulesetSetting.ScrollDirection, configDirection);
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
- Config.BindWith(ManiaRulesetSetting.ScrollTime, TimeRange);
+ Config.BindWith(ManiaRulesetSetting.ScrollTime, configTimeRange);
}
- ///
- /// Retrieves the column that intersects a screen-space position.
- ///
- /// The screen-space position.
- /// The column which intersects with .
- public Column GetColumnByPosition(Vector2 screenSpacePosition) => Playfield.GetColumnByPosition(screenSpacePosition);
+ protected override void AdjustScrollSpeed(int amount)
+ {
+ this.TransformTo(nameof(relativeTimeRange), relativeTimeRange + amount, 200, Easing.OutQuint);
+ }
+
+ private double relativeTimeRange
+ {
+ get => MAX_TIME_RANGE / configTimeRange.Value;
+ set => configTimeRange.Value = MAX_TIME_RANGE / value;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ updateTimeRange();
+ }
+
+ private void updateTimeRange() => TimeRange.Value = configTimeRange.Value * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value;
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer();
@@ -85,5 +131,7 @@ namespace osu.Game.Rulesets.Mania.UI
}
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay);
+
+ protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new ManiaReplayRecorder(replay);
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index 08f6049782..271e432e8d 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
@@ -6,6 +6,7 @@ using osu.Framework.Graphics.Containers;
using System;
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Allocation;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@@ -14,9 +15,12 @@ using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
+ [Cached]
public class ManiaPlayfield : ScrollingPlayfield
{
- private readonly List stages = new List();
+ public IReadOnlyList Stages => stages;
+
+ private readonly List stages = new List();
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos));
@@ -41,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.UI
for (int i = 0; i < stageDefinitions.Count; i++)
{
- var newStage = new ManiaStage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction);
+ var newStage = new Stage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction);
playfieldGrid.Content[0][i] = newStage;
@@ -71,7 +75,7 @@ namespace osu.Game.Rulesets.Mania.UI
{
foreach (var column in stage.Columns)
{
- if (column.ReceivePositionalInputAt(screenSpacePosition))
+ if (column.ReceivePositionalInputAt(new Vector2(screenSpacePosition.X, column.ScreenSpaceDrawQuad.Centre.Y)))
{
found = column;
break;
@@ -85,12 +89,37 @@ namespace osu.Game.Rulesets.Mania.UI
return found;
}
+ ///
+ /// Retrieves a by index.
+ ///
+ /// The index of the column.
+ /// The corresponding to the given index.
+ /// If is less than 0 or greater than .
+ public Column GetColumn(int index)
+ {
+ if (index < 0 || index > TotalColumns - 1)
+ throw new ArgumentOutOfRangeException(nameof(index));
+
+ foreach (var stage in stages)
+ {
+ if (index >= stage.Columns.Count)
+ {
+ index -= stage.Columns.Count;
+ continue;
+ }
+
+ return stage.Columns[index];
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
///
/// Retrieves the total amount of columns across all stages in this playfield.
///
public int TotalColumns => stages.Sum(s => s.Columns.Count);
- private ManiaStage getStageByColumn(int column)
+ private Stage getStageByColumn(int column)
{
int sum = 0;
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs
index d893a3fdde..30e0aafb7d 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs
@@ -3,7 +3,6 @@
using osu.Framework.Graphics;
using osu.Game.Rulesets.UI;
-using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -13,8 +12,6 @@ namespace osu.Game.Rulesets.Mania.UI
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
-
- Size = new Vector2(1, 0.8f);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs b/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs
new file mode 100644
index 0000000000..18275000a2
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.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 System.Collections.Generic;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Mania.Replays;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.UI
+{
+ public class ManiaReplayRecorder : ReplayRecorder
+ {
+ public ManiaReplayRecorder(Replay replay)
+ : base(replay)
+ {
+ }
+
+ protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame)
+ => new ManiaReplayFrame(Time.Current, actions.ToArray());
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
similarity index 61%
rename from osu.Game.Rulesets.Mania/UI/ManiaStage.cs
rename to osu.Game.Rulesets.Mania/UI/Stage.cs
index a28de7ea58..faa04dea97 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -1,20 +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 System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Skinning;
+using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@@ -23,30 +24,33 @@ namespace osu.Game.Rulesets.Mania.UI
///
/// A collection of s.
///
- public class ManiaStage : ScrollingPlayfield
+ public class Stage : ScrollingPlayfield
{
public const float COLUMN_SPACING = 1;
- public const float HIT_TARGET_POSITION = 50;
+ public const float HIT_TARGET_POSITION = 110;
public IReadOnlyList Columns => columnFlow.Children;
private readonly FillFlowContainer columnFlow;
- private readonly Container barLineContainer;
-
public Container Judgements => judgements;
private readonly JudgementContainer judgements;
+ private readonly Drawable barLineContainer;
private readonly Container topLevelContainer;
- private List normalColumnColours = new List();
- private Color4 specialColumnColour;
+ private readonly Dictionary columnColours = new Dictionary
+ {
+ { ColumnType.Even, new Color4(6, 84, 0, 255) },
+ { ColumnType.Odd, new Color4(94, 0, 57, 255) },
+ { ColumnType.Special, new Color4(0, 48, 63, 255) }
+ };
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Columns.Any(c => c.ReceivePositionalInputAt(screenSpacePos));
private readonly int firstColumnIndex;
- public ManiaStage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
+ public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
{
this.firstColumnIndex = firstColumnIndex;
@@ -67,31 +71,17 @@ namespace osu.Game.Rulesets.Mania.UI
AutoSizeAxes = Axes.X,
Children = new Drawable[]
{
- new Container
+ new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground())
{
- Name = "Columns mask",
+ RelativeSizeAxes = Axes.Both
+ },
+ columnFlow = new FillFlowContainer
+ {
+ Name = "Columns",
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
- Masking = true,
- CornerRadius = 5,
- Children = new Drawable[]
- {
- new Box
- {
- Name = "Background",
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.Black
- },
- columnFlow = new FillFlowContainer
- {
- Name = "Columns",
- RelativeSizeAxes = Axes.Y,
- AutoSizeAxes = Axes.X,
- Direction = FillDirection.Horizontal,
- Padding = new MarginPadding { Left = COLUMN_SPACING, Right = COLUMN_SPACING },
- Spacing = new Vector2(COLUMN_SPACING, 0)
- },
- }
+ Direction = FillDirection.Horizontal,
+ Padding = new MarginPadding { Left = COLUMN_SPACING, Right = COLUMN_SPACING },
},
new Container
{
@@ -102,22 +92,24 @@ namespace osu.Game.Rulesets.Mania.UI
Width = 1366, // Bar lines should only be masked on the vertical axis
BypassAutoSizeAxes = Axes.Both,
Masking = true,
- Child = barLineContainer = new Container
+ Child = barLineContainer = new HitObjectArea(HitObjectContainer)
{
Name = "Bar lines",
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
- Child = HitObjectContainer
}
},
+ new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null)
+ {
+ RelativeSizeAxes = Axes.Both
+ },
judgements = new JudgementContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
- AutoSizeAxes = Axes.Both,
+ RelativeSizeAxes = Axes.Both,
Y = HIT_TARGET_POSITION + 150,
- BypassAutoSizeAxes = Axes.Both
},
topLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
}
@@ -126,24 +118,52 @@ namespace osu.Game.Rulesets.Mania.UI
for (int i = 0; i < definition.Columns; i++)
{
- var isSpecial = definition.IsSpecialColumn(i);
+ var columnType = definition.GetTypeOfColumn(i);
var column = new Column(firstColumnIndex + i)
{
- IsSpecial = isSpecial,
- Action = { Value = isSpecial ? specialColumnStartAction++ : normalColumnStartAction++ }
+ ColumnType = columnType,
+ AccentColour = columnColours[columnType],
+ Action = { Value = columnType == ColumnType.Special ? specialColumnStartAction++ : normalColumnStartAction++ }
};
AddColumn(column);
}
+ }
- Direction.BindValueChanged(dir =>
+ private ISkin currentSkin;
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ currentSkin = skin;
+ skin.SourceChanged += onSkinChanged;
+
+ onSkinChanged();
+ }
+
+ private void onSkinChanged()
+ {
+ foreach (var col in columnFlow)
{
- barLineContainer.Padding = new MarginPadding
+ if (col.Index > 0)
{
- Top = dir.NewValue == ScrollingDirection.Up ? HIT_TARGET_POSITION : 0,
- Bottom = dir.NewValue == ScrollingDirection.Down ? HIT_TARGET_POSITION : 0,
- };
- }, true);
+ float spacing = currentSkin.GetConfig(
+ new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, col.Index - 1))
+ ?.Value ?? COLUMN_SPACING;
+
+ col.Margin = new MarginPadding { Left = spacing };
+ }
+
+ float? width = currentSkin.GetConfig(
+ new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, col.Index))
+ ?.Value;
+
+ if (width == null)
+ // only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration)
+ col.Width = col.IsSpecial ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH;
+ else
+ col.Width = width.Value;
+ }
}
public void AddColumn(Column c)
@@ -196,38 +216,6 @@ namespace osu.Game.Rulesets.Mania.UI
});
}
- [BackgroundDependencyLoader]
- private void load()
- {
- normalColumnColours = new List
- {
- new Color4(94, 0, 57, 255),
- new Color4(6, 84, 0, 255)
- };
-
- specialColumnColour = new Color4(0, 48, 63, 255);
-
- // Set the special column + colour + key
- foreach (var column in Columns)
- {
- if (!column.IsSpecial)
- continue;
-
- column.AccentColour = specialColumnColour;
- }
-
- var nonSpecialColumns = Columns.Where(c => !c.IsSpecial).ToList();
-
- // We'll set the colours of the non-special columns in a separate loop, because the non-special
- // column colours are mirrored across their centre and special styles mess with this
- for (int i = 0; i < Math.Ceiling(nonSpecialColumns.Count / 2f); i++)
- {
- Color4 colour = normalColumnColours[i % normalColumnColours.Count];
- nonSpecialColumns[i].AccentColour = colour;
- nonSpecialColumns[nonSpecialColumns.Count - 1 - i].AccentColour = colour;
- }
- }
-
protected override void Update()
{
// Due to masking differences, it is not possible to get the width of the columns container automatically
diff --git a/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs b/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs
new file mode 100644
index 0000000000..878d1088a6
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs
@@ -0,0 +1,61 @@
+// 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.Input.Bindings;
+
+namespace osu.Game.Rulesets.Mania
+{
+ public class VariantMappingGenerator
+ {
+ ///
+ /// All the s available to the left hand.
+ ///
+ public InputKey[] LeftKeys;
+
+ ///
+ /// All the s available to the right hand.
+ ///
+ public InputKey[] RightKeys;
+
+ ///
+ /// The for the special key.
+ ///
+ public InputKey SpecialKey;
+
+ ///
+ /// The at which the normal columns should begin.
+ ///
+ public ManiaAction NormalActionStart;
+
+ ///
+ /// The for the special column.
+ ///
+ public ManiaAction SpecialAction;
+
+ ///
+ /// Generates a list of s for a specific number of columns.
+ ///
+ /// The number of columns that need to be bound.
+ /// The next to use for normal columns.
+ /// The keybindings.
+ public IEnumerable GenerateKeyBindingsFor(int columns, out ManiaAction nextNormalAction)
+ {
+ ManiaAction currentNormalAction = NormalActionStart;
+
+ var bindings = new List();
+
+ for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++)
+ bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++));
+
+ if (columns % 2 == 1)
+ bindings.Add(new KeyBinding(SpecialKey, SpecialAction));
+
+ for (int i = 0; i < columns / 2; i++)
+ bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++));
+
+ nextNormalAction = currentNormalAction;
+ return bindings;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs b/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs
new file mode 100644
index 0000000000..7697f46160
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.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.Osu.Tests.Mods
+{
+ public class OsuModTestScene : ModTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs
new file mode 100644
index 0000000000..49c1fe8540
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs
@@ -0,0 +1,80 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModDifficultyAdjust : OsuModTestScene
+ {
+ [Test]
+ public void TestNoAdjustment() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModDifficultyAdjust(),
+ Autoplay = true,
+ PassCondition = checkSomeHit
+ });
+
+ [Test]
+ public void TestCircleSize1() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModDifficultyAdjust { CircleSize = { Value = 1 } },
+ Autoplay = true,
+ PassCondition = () => checkSomeHit() && checkObjectsScale(0.78f)
+ });
+
+ [Test]
+ public void TestCircleSize10() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModDifficultyAdjust { CircleSize = { Value = 10 } },
+ Autoplay = true,
+ PassCondition = () => checkSomeHit() && checkObjectsScale(0.15f)
+ });
+
+ [Test]
+ public void TestApproachRate1() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModDifficultyAdjust { ApproachRate = { Value = 1 } },
+ Autoplay = true,
+ PassCondition = () => checkSomeHit() && checkObjectsPreempt(1680)
+ });
+
+ [Test]
+ public void TestApproachRate10() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } },
+ Autoplay = true,
+ PassCondition = () => checkSomeHit() && checkObjectsPreempt(450)
+ });
+
+ private bool checkObjectsPreempt(double target)
+ {
+ var objects = Player.ChildrenOfType();
+ if (!objects.Any())
+ return false;
+
+ return objects.All(o => o.HitObject.TimePreempt == target);
+ }
+
+ private bool checkObjectsScale(float target)
+ {
+ var objects = Player.ChildrenOfType();
+ if (!objects.Any())
+ return false;
+
+ return objects.All(o => Precision.AlmostEquals(o.ChildrenOfType().First().Children.OfType().Single().Scale.X, target));
+ }
+
+ private bool checkSomeHit()
+ {
+ return Player.ScoreProcessor.JudgedHits >= 2;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs
new file mode 100644
index 0000000000..335ef31019
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Osu.Mods;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModDoubleTime : OsuModTestScene
+ {
+ [TestCase(0.5)]
+ [TestCase(1.01)]
+ [TestCase(1.5)]
+ [TestCase(2)]
+ [TestCase(5)]
+ public void TestSpeedChangeCustomisation(double rate)
+ {
+ var mod = new OsuModDoubleTime { SpeedChange = { Value = rate } };
+
+ CreateModTest(new ModTestData
+ {
+ Mod = mod,
+ PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 &&
+ Precision.AlmostEquals(Player.GameplayClockContainer.GameplayClock.Rate, mod.SpeedChange.Value)
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
new file mode 100644
index 0000000000..40f1c4a52f
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
@@ -0,0 +1,100 @@
+// Copyright (c) ppy Pty Ltd