diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 58c24181d3..97fcb52ab1 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -2,12 +2,6 @@
"version": 1,
"isRoot": true,
"tools": {
- "cake.tool": {
- "version": "0.35.0",
- "commands": [
- "dotnet-cake"
- ]
- },
"dotnet-format": {
"version": "3.1.37601",
"commands": [
@@ -20,17 +14,23 @@
"jb"
]
},
- "nvika": {
- "version": "2.0.0",
+ "smoogipoo.nvika": {
+ "version": "1.0.1",
"commands": [
"nvika"
]
},
"codefilesanity": {
- "version": "15.0.0",
+ "version": "0.0.36",
"commands": [
"CodeFileSanity"
]
+ },
+ "ppy.localisationanalyser.tools": {
+ "version": "2021.705.0",
+ "commands": [
+ "localisation"
+ ]
}
}
}
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
index 0cdf3b92d3..19bd89c52f 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -10,14 +10,6 @@ trim_trailing_whitespace = true
#Roslyn naming styles
-#PascalCase for public and protected members
-dotnet_naming_style.pascalcase.capitalization = pascal_case
-dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
-dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event
-dotnet_naming_rule.public_members_pascalcase.severity = error
-dotnet_naming_rule.public_members_pascalcase.symbols = public_members
-dotnet_naming_rule.public_members_pascalcase.style = pascalcase
-
#camelCase for private members
dotnet_naming_style.camelcase.capitalization = camel_case
@@ -165,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning
#Style - variable declaration
csharp_style_inlined_variable_declaration = true:warning
-csharp_style_deconstructed_variable_declaration = true:warning
+csharp_style_deconstructed_variable_declaration = false:silent
#Style - other C# 7.x features
dotnet_style_prefer_inferred_tuple_names = true:warning
@@ -176,8 +168,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
#Style - C# 8 features
csharp_prefer_static_local_function = true:warning
csharp_prefer_simple_using_statement = true:silent
-csharp_style_prefer_index_operator = true:warning
-csharp_style_prefer_range_operator = true:warning
+csharp_style_prefer_index_operator = false:silent
+csharp_style_prefer_range_operator = false:silent
csharp_style_prefer_switch_expression = false:none
#Supressing roslyn built-in analyzers
@@ -197,4 +189,4 @@ dotnet_diagnostic.IDE0069.severity = none
dotnet_diagnostic.CA2225.severity = none
# Banned APIs
-dotnet_diagnostic.RS0030.severity = error
\ No newline at end of file
+dotnet_diagnostic.RS0030.severity = error
diff --git a/.github/ISSUE_TEMPLATE/02-feature-request-issues.md b/.github/ISSUE_TEMPLATE/02-feature-request-issues.md
deleted file mode 100644
index c3357dd780..0000000000
--- a/.github/ISSUE_TEMPLATE/02-feature-request-issues.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-name: Feature Request
-about: Propose a feature you would like to see in the game!
----
-**Describe the new feature:**
-
-**Proposal designs of the feature:**
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 69baeee60c..c62231e8e0 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,5 +1,12 @@
blank_issues_enabled: false
contact_links:
+ - name: Suggestions or feature request
+ url: https://github.com/ppy/osu/discussions/categories/ideas
+ about: Got something you think should change or be added? Search for or start a new discussion!
+ - name: Help
+ url: https://github.com/ppy/osu/discussions/categories/q-a
+ about: osu! not working as you'd expect? Not sure it's a bug? Check the Q&A section!
- name: osu!stable issues
url: https://github.com/ppy/osu-stable-issues
- about: For issues regarding osu!stable (not osu!lazer), open them here.
+ about: For osu!stable bugs (not osu!lazer), check out the dedicated repository. Note that we only accept serious bug reports.
+
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000000..29cbdd2d37
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,94 @@
+on: [push, pull_request]
+name: Continuous Integration
+
+jobs:
+ test:
+ name: Test
+ runs-on: ${{matrix.os.fullname}}
+ env:
+ OSU_EXECUTION_MODE: ${{matrix.threadingMode}}
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - { prettyname: Windows, fullname: windows-latest }
+ - { prettyname: macOS, fullname: macos-latest }
+ - { prettyname: Linux, fullname: ubuntu-latest }
+ threadingMode: ['SingleThread', 'MultiThreaded']
+ timeout-minutes: 60
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Install .NET 5.0.x
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: "5.0.x"
+
+ # FIXME: libavformat is not included in Ubuntu. Let's fix that.
+ # https://github.com/ppy/osu-framework/issues/4349
+ # Remove this once https://github.com/actions/virtual-environments/issues/3306 has been resolved.
+ - name: Install libavformat-dev
+ if: ${{matrix.os.fullname == 'ubuntu-latest'}}
+ run: |
+ sudo apt-get update && \
+ sudo apt-get -y install libavformat-dev
+
+ - name: Compile
+ run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
+
+ - name: Test
+ run: dotnet test $pwd/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx"
+ shell: pwsh
+
+ # Attempt to upload results even if test fails.
+ # https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
+ - name: Upload Test Results
+ uses: actions/upload-artifact@v2
+ if: ${{ always() }}
+ with:
+ name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
+ path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx
+
+ inspect-code:
+ name: Code Quality
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ # FIXME: Tools won't run in .NET 5.0 unless you install 3.1.x LTS side by side.
+ # https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
+ - name: Install .NET 3.1.x LTS
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: "3.1.x"
+
+ - name: Install .NET 5.0.x
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: "5.0.x"
+
+ - name: Restore Tools
+ run: dotnet tool restore
+
+ - name: Restore Packages
+ run: dotnet restore
+
+ - name: CodeFileSanity
+ run: |
+ # TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
+ # FIXME: Suppress warnings from templates project
+ dotnet codefilesanity | while read -r line; do
+ echo "::warning::$line"
+ done
+
+ # Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
+ # - name: .NET Format (Dry Run)
+ # run: dotnet format --dry-run --check
+
+ - name: InspectCode
+ run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --output=$(pwd)/inspectcodereport.xml --cachesDir=$(pwd)/inspectcode --verbosity=WARN
+
+ - name: NVika
+ run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml
new file mode 100644
index 0000000000..e0ccd50989
--- /dev/null
+++ b/.github/workflows/report-nunit.yml
@@ -0,0 +1,32 @@
+# This is a workaround to allow PRs to report their coverage. This will run inside the base repository.
+# See:
+# * https://github.com/dorny/test-reporter#recommended-setup-for-public-repositories
+# * https://docs.github.com/en/actions/reference/authentication-in-a-workflow#permissions-for-the-github_token
+name: Annotate CI run with test results
+on:
+ workflow_run:
+ workflows: ["Continuous Integration"]
+ types:
+ - completed
+jobs:
+ annotate:
+ name: Annotate CI run with test results
+ runs-on: ubuntu-latest
+ if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - { prettyname: Windows }
+ - { prettyname: macOS }
+ - { prettyname: Linux }
+ threadingMode: ['SingleThread', 'MultiThreaded']
+ timeout-minutes: 5
+ steps:
+ - name: Annotate CI run with test results
+ uses: dorny/test-reporter@v1.4.2
+ with:
+ artifact: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
+ name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
+ path: "*.trx"
+ reporter: dotnet-trx
diff --git a/.gitignore b/.gitignore
index d122d25054..de6a3ac848 100644
--- a/.gitignore
+++ b/.gitignore
@@ -336,3 +336,6 @@ inspectcode
/BenchmarkDotNet.Artifacts
*.GeneratedMSBuildEditorConfig.editorconfig
+
+# Fody (pulled in by Realm) - schema file
+FodyWeavers.xsd
diff --git a/.idea/.idea.osu.Desktop/.idea/dataSources.xml b/.idea/.idea.osu.Desktop/.idea/dataSources.xml
deleted file mode 100644
index 10f8c1c84d..0000000000
--- a/.idea/.idea.osu.Desktop/.idea/dataSources.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- sqlite.xerial
- true
- org.sqlite.JDBC
- jdbc:sqlite:$USER_HOME$/.local/share/osu/client.db
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index afd997f91d..1b590008cd 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -113,20 +113,6 @@
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build benchmarks",
"console": "internalConsole"
- },
- {
- "name": "Cake: Debug Script",
- "type": "coreclr",
- "request": "launch",
- "program": "${workspaceRoot}/build/tools/Cake.CoreCLR/0.30.0/Cake.dll",
- "args": [
- "${workspaceRoot}/build/build.cake",
- "--debug",
- "--verbosity=diagnostic"
- ],
- "cwd": "${workspaceRoot}/build",
- "stopAtEntry": true,
- "externalConsole": false
}
]
}
diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index 46c50dbfa2..ea3e25142c 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -3,6 +3,7 @@ M:System.Object.Equals(System.Object)~System.Boolean;Don't use object.Equals. Us
M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(Fallbacks to ValueType). Use IEquatable or EqualityComparer.Default instead.
M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead.
T:System.IComparable;Don't use non-generic IComparable. Use generic version instead.
+T:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable instead.
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
diff --git a/FodyWeavers.xml b/FodyWeavers.xml
new file mode 100644
index 0000000000..ea490e3297
--- /dev/null
+++ b/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/InspectCode.ps1 b/InspectCode.ps1
index 6ed935fdbb..8316f48ff3 100644
--- a/InspectCode.ps1
+++ b/InspectCode.ps1
@@ -1,27 +1,11 @@
-[CmdletBinding()]
-Param(
- [string]$Target,
- [string]$Configuration,
- [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
- [string]$Verbosity,
- [switch]$ShowDescription,
- [Alias("WhatIf", "Noop")]
- [switch]$DryRun,
- [Parameter(Position = 0, Mandatory = $false, ValueFromRemainingArguments = $true)]
- [string[]]$ScriptArgs
-)
-
-# Build Cake arguments
-$cakeArguments = "";
-if ($Target) { $cakeArguments += "-target=$Target" }
-if ($Configuration) { $cakeArguments += "-configuration=$Configuration" }
-if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" }
-if ($ShowDescription) { $cakeArguments += "-showdescription" }
-if ($DryRun) { $cakeArguments += "-dryrun" }
-if ($Experimental) { $cakeArguments += "-experimental" }
-$cakeArguments += $ScriptArgs
-
dotnet tool restore
-dotnet cake ./build/InspectCode.cake --bootstrap
-dotnet cake ./build/InspectCode.cake $cakeArguments
-exit $LASTEXITCODE
\ No newline at end of file
+
+# Temporarily disabled until the tool is upgraded to 5.0.
+ # The version specified in .config/dotnet-tools.json (3.1.37601) won't run on .NET hosts >=5.0.7.
+ # - cmd: dotnet format --dry-run --check
+
+dotnet CodeFileSanity
+dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
+dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
+
+exit $LASTEXITCODE
diff --git a/InspectCode.sh b/InspectCode.sh
new file mode 100755
index 0000000000..cf2bc18175
--- /dev/null
+++ b/InspectCode.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+dotnet tool restore
+dotnet CodeFileSanity
+dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
+dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
diff --git a/README.md b/README.md
index eb790ca18f..016bd7d922 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
A free-to-win rhythm game. Rhythm is just a *click* away!
-The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew.
+The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the codename "*lazer*". As in sharper than cutting-edge.
## Status
@@ -23,7 +23,7 @@ We are accepting bug reports (please report with as much detail as possible and
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
-- Read peppy's [latest blog post](https://blog.ppy.sh/a-definitive-lazer-faq/) exploring where lazer is currently and the roadmap going forward.
+- Read peppy's [blog post](https://blog.ppy.sh/a-definitive-lazer-faq/) exploring where the project is currently and the roadmap going forward.
## Running osu!
@@ -43,7 +43,7 @@ If your platform is not listed above, there is still a chance you can manually b
osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates).
-You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/issues/5852).
+You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/discussions/13096).
## Developing osu!
@@ -97,7 +97,7 @@ Before committing your code, please run a code formatter. This can be achieved b
We have adopted some cross-platform, compiler integrated analyzers. They can provide warnings when you are editing, building inside IDE or from command line, as-if they are provided by the compiler itself.
-JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it from PowerShell with `.\InspectCode.ps1`, which is [only supported on Windows](https://youtrack.jetbrains.com/issue/RSRP-410004). Alternatively, you can install ReSharper or use Rider to get inline support in your IDE of choice.
+JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it from PowerShell with `.\InspectCode.ps1`. Alternatively, you can install ReSharper or use Rider to get inline support in your IDE of choice.
## Contributing
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
index 992f954a3a..3dd6be7307 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -10,9 +10,9 @@
-
+
-
+
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs
index 59a68245a6..a80f1178b6 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformDifficultyCalculator.cs
@@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.EmptyFreeform
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty();
- protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0];
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
}
}
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs
index d5c1e9bd15..f705009d18 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
-using osu.Game.Rulesets.EmptyFreeform.Objects;
using osu.Game.Rulesets.EmptyFreeform.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
@@ -11,7 +10,7 @@ using osu.Game.Users;
namespace osu.Game.Rulesets.EmptyFreeform.Mods
{
- public class EmptyFreeformModAutoplay : ModAutoplay
+ public class EmptyFreeformModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformAutoGenerator.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformAutoGenerator.cs
index 6d8d4215a2..62f394d1ce 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformAutoGenerator.cs
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Replays/EmptyFreeformAutoGenerator.cs
@@ -1,28 +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 System.Collections.Generic;
using osu.Game.Beatmaps;
-using osu.Game.Replays;
using osu.Game.Rulesets.EmptyFreeform.Objects;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.EmptyFreeform.Replays
{
- public class EmptyFreeformAutoGenerator : AutoGenerator
+ public class EmptyFreeformAutoGenerator : AutoGenerator
{
- protected Replay Replay;
- protected List Frames => Replay.Frames;
-
public new Beatmap Beatmap => (Beatmap)base.Beatmap;
public EmptyFreeformAutoGenerator(IBeatmap beatmap)
: base(beatmap)
{
- Replay = new Replay();
}
- public override Replay Generate()
+ protected override void GenerateFrames()
{
Frames.Add(new EmptyFreeformReplayFrame());
@@ -35,8 +29,6 @@ namespace osu.Game.Rulesets.EmptyFreeform.Replays
// todo: add required inputs and extra frames.
});
}
-
- return Replay;
}
}
}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 7571d1827a..0c4bfe0ed7 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -10,9 +10,9 @@
-
+
-
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
index 8ea334c99c..4565c97d1a 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
@@ -4,14 +4,13 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.Replays;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Rulesets.Pippidon.Mods
{
- public class PippidonModAutoplay : ModAutoplay
+ public class PippidonModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
index f6340f6c25..290148d14b 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
@@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty();
- protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0];
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
}
}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs
index 9c54b82e38..612288257d 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs
@@ -1,28 +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 System.Collections.Generic;
using osu.Game.Beatmaps;
-using osu.Game.Replays;
using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Pippidon.Replays
{
- public class PippidonAutoGenerator : AutoGenerator
+ public class PippidonAutoGenerator : AutoGenerator
{
- protected Replay Replay;
- protected List Frames => Replay.Frames;
-
public new Beatmap Beatmap => (Beatmap)base.Beatmap;
public PippidonAutoGenerator(IBeatmap beatmap)
: base(beatmap)
{
- Replay = new Replay();
}
- public override Replay Generate()
+ protected override void GenerateFrames()
{
Frames.Add(new PippidonReplayFrame());
@@ -34,8 +28,6 @@ namespace osu.Game.Rulesets.Pippidon.Replays
Position = hitObject.Position,
});
}
-
- return Replay;
}
}
}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
index 1c8ed54440..bb0a487274 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -10,9 +10,9 @@
-
+
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs
index 7f29c4e712..f557a4c754 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingDifficultyCalculator.cs
@@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.EmptyScrolling
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty();
- protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0];
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
}
}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs
index 6dad1ff43b..431994e098 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs
@@ -3,7 +3,6 @@
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.EmptyScrolling.Objects;
using osu.Game.Rulesets.EmptyScrolling.Replays;
using osu.Game.Scoring;
using osu.Game.Users;
@@ -11,7 +10,7 @@ using System.Collections.Generic;
namespace osu.Game.Rulesets.EmptyScrolling.Mods
{
- public class EmptyScrollingModAutoplay : ModAutoplay
+ public class EmptyScrollingModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingAutoGenerator.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingAutoGenerator.cs
index 7923918842..1058f756f3 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingAutoGenerator.cs
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Replays/EmptyScrollingAutoGenerator.cs
@@ -1,28 +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 System.Collections.Generic;
using osu.Game.Beatmaps;
-using osu.Game.Replays;
using osu.Game.Rulesets.EmptyScrolling.Objects;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.EmptyScrolling.Replays
{
- public class EmptyScrollingAutoGenerator : AutoGenerator
+ public class EmptyScrollingAutoGenerator : AutoGenerator
{
- protected Replay Replay;
- protected List Frames => Replay.Frames;
-
public new Beatmap Beatmap => (Beatmap)base.Beatmap;
public EmptyScrollingAutoGenerator(IBeatmap beatmap)
: base(beatmap)
{
- Replay = new Replay();
}
- public override Replay Generate()
+ protected override void GenerateFrames()
{
Frames.Add(new EmptyScrollingReplayFrame());
@@ -34,8 +28,6 @@ namespace osu.Game.Rulesets.EmptyScrolling.Replays
// todo: add required inputs and extra frames.
});
}
-
- return Replay;
}
}
}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 7571d1827a..0c4bfe0ed7 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -10,9 +10,9 @@
-
+
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
index 8ea334c99c..4565c97d1a 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs
@@ -4,14 +4,13 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.Replays;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Rulesets.Pippidon.Mods
{
- public class PippidonModAutoplay : ModAutoplay
+ public class PippidonModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
index f6340f6c25..290148d14b 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonDifficultyCalculator.cs
@@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty();
- protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0];
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
}
}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs
index bd99cdcdbd..724026273d 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Replays/PippidonAutoGenerator.cs
@@ -2,29 +2,23 @@
// 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;
using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.UI;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Pippidon.Replays
{
- public class PippidonAutoGenerator : AutoGenerator
+ public class PippidonAutoGenerator : AutoGenerator
{
- protected Replay Replay;
- protected List Frames => Replay.Frames;
-
public new Beatmap Beatmap => (Beatmap)base.Beatmap;
public PippidonAutoGenerator(IBeatmap beatmap)
: base(beatmap)
{
- Replay = new Replay();
}
- public override Replay Generate()
+ protected override void GenerateFrames()
{
int currentLane = 0;
@@ -55,8 +49,6 @@ namespace osu.Game.Rulesets.Pippidon.Replays
currentLane = hitObject.Lane;
}
-
- return Replay;
}
private void addFrame(double time, PippidonAction direction)
diff --git a/appveyor.yml b/appveyor.yml
index a4a0cedc66..5be73f9875 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,24 +1,27 @@
clone_depth: 1
version: '{branch}-{build}'
image: Visual Studio 2019
+cache:
+ - '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml'
+
dotnet_csproj:
patch: true
file: 'osu.Game\osu.Game.csproj' # Use wildcard when it's able to exclude Xamarin projects
version: '0.0.{build}'
-cache:
- - '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml'
+
before_build:
- - ps: dotnet --info # Useful when version mismatch between CI and local
- - ps: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
+ - cmd: dotnet --info # Useful when version mismatch between CI and local
+ - cmd: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
+
build:
project: osu.sln
parallel: true
verbosity: minimal
publish_nuget: true
+
after_build:
- - ps: dotnet tool restore
- - ps: dotnet format --dry-run --check
- ps: .\InspectCode.ps1
+
test:
assemblies:
except:
diff --git a/build/Desktop.proj b/build/Desktop.proj
deleted file mode 100644
index b1c6b065e8..0000000000
--- a/build/Desktop.proj
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/build/InspectCode.cake b/build/InspectCode.cake
deleted file mode 100644
index 6836d9071b..0000000000
--- a/build/InspectCode.cake
+++ /dev/null
@@ -1,41 +0,0 @@
-#addin "nuget:?package=CodeFileSanity&version=0.0.36"
-
-///////////////////////////////////////////////////////////////////////////////
-// ARGUMENTS
-///////////////////////////////////////////////////////////////////////////////
-
-var target = Argument("target", "CodeAnalysis");
-var configuration = Argument("configuration", "Release");
-
-var rootDirectory = new DirectoryPath("..");
-var sln = rootDirectory.CombineWithFilePath("osu.sln");
-var desktopSlnf = rootDirectory.CombineWithFilePath("osu.Desktop.slnf");
-
-///////////////////////////////////////////////////////////////////////////////
-// TASKS
-///////////////////////////////////////////////////////////////////////////////
-
-Task("InspectCode")
- .Does(() => {
- var inspectcodereport = "inspectcodereport.xml";
- var cacheDir = "inspectcode";
- var verbosity = AppVeyor.IsRunningOnAppVeyor ? "WARN" : "INFO"; // Don't flood CI output
-
- DotNetCoreTool(rootDirectory.FullPath,
- "jb", $@"inspectcode ""{desktopSlnf}"" --output=""{inspectcodereport}"" --caches-home=""{cacheDir}"" --verbosity={verbosity}");
- DotNetCoreTool(rootDirectory.FullPath, "nvika", $@"parsereport ""{inspectcodereport}"" --treatwarningsaserrors");
- });
-
-Task("CodeFileSanity")
- .Does(() => {
- ValidateCodeSanity(new ValidateCodeSanitySettings {
- RootDirectory = rootDirectory.FullPath,
- IsAppveyorBuild = AppVeyor.IsRunningOnAppVeyor
- });
- });
-
-Task("CodeAnalysis")
- .IsDependentOn("CodeFileSanity")
- .IsDependentOn("InspectCode");
-
-RunTarget(target);
\ No newline at end of file
diff --git a/cake.config b/cake.config
deleted file mode 100644
index 187d825591..0000000000
--- a/cake.config
+++ /dev/null
@@ -1,5 +0,0 @@
-
-[Nuget]
-Source=https://api.nuget.org/v3/index.json
-UseInProcessClient=true
-LoadDependencies=true
diff --git a/osu.Android.props b/osu.Android.props
index 5aee9e15cc..171a0862a1 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,11 @@
-
-
+
+
+
+
+
+
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index cffcea22c2..063e02d349 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -20,7 +20,8 @@ namespace osu.Android
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
- [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed" })]
+ [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-archive")]
+ [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed", "application/x-osu-archive" })]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
public class OsuGameActivity : AndroidGameActivity
{
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index 4a28ab3722..4de1e84fbf 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -57,7 +57,7 @@ namespace osu.Desktop
private string getStableInstallPath()
{
- static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
+ static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs")) || File.Exists(Path.Combine(p, "osu!.cfg"));
string stableInstallPath;
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index 47cd39dc5a..910751a723 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -68,6 +68,8 @@ namespace osu.Desktop.Updater
return false;
}
+ scheduleRecheck = false;
+
if (notification == null)
{
notification = new UpdateProgressNotification(this) { State = ProgressNotificationState.Active };
@@ -98,7 +100,6 @@ namespace osu.Desktop.Updater
// 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).ConfigureAwait(false);
- scheduleRecheck = false;
}
else
{
@@ -110,13 +111,14 @@ namespace osu.Desktop.Updater
catch (Exception)
{
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
+ scheduleRecheck = true;
}
finally
{
if (scheduleRecheck)
{
// check again in 30 minutes.
- Scheduler.AddDelayed(async () => await checkForUpdateAsync().ConfigureAwait(false), 60000 * 30);
+ Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
}
}
@@ -141,7 +143,7 @@ namespace osu.Desktop.Updater
Activated = () =>
{
updateManager.PrepareUpdateAsync()
- .ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
+ .ContinueWith(_ => updateManager.Schedule(() => game?.GracefullyExit()));
return true;
};
}
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index ad5c323e9b..89b9ffb94b 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -5,8 +5,8 @@
true
A free-to-win rhythm game. Rhythm is just a *click* away!
osu!
- osu!lazer
- osu!lazer
+ osu!
+ osu!(lazer)
lazer.ico
app.manifest
0.0.0
diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec
index fa182f8e70..1757fd7c73 100644
--- a/osu.Desktop/osu.nuspec
+++ b/osu.Desktop/osu.nuspec
@@ -3,7 +3,7 @@
osulazer
0.0.0
- osu!lazer
+ osu!
ppy Pty Ltd
Dean Herbert
https://osu.ppy.sh/
@@ -20,4 +20,3 @@
-
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index bfcf4ef35e..da8a0540f4 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -7,9 +7,9 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchEditorTestSceneContainer.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchEditorTestSceneContainer.cs
new file mode 100644
index 0000000000..158c8edba5
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchEditorTestSceneContainer.cs
@@ -0,0 +1,66 @@
+// 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.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Edit;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ public class CatchEditorTestSceneContainer : Container
+ {
+ [Cached(typeof(Playfield))]
+ public readonly ScrollingPlayfield Playfield;
+
+ protected override Container Content { get; }
+
+ public CatchEditorTestSceneContainer()
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ Width = CatchPlayfield.WIDTH;
+ Height = 1000;
+ Padding = new MarginPadding
+ {
+ Bottom = 100
+ };
+
+ InternalChildren = new Drawable[]
+ {
+ new ScrollingTestContainer(ScrollingDirection.Down)
+ {
+ TimeRange = 1000,
+ RelativeSizeAxes = Axes.Both,
+ Child = Playfield = new TestCatchPlayfield
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ },
+ new PlayfieldBorder
+ {
+ PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Full },
+ Clock = new FramedClock(new StopwatchClock(true))
+ },
+ Content = new Container
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ };
+ }
+
+ private class TestCatchPlayfield : CatchEditorPlayfield
+ {
+ public TestCatchPlayfield()
+ : base(new BeatmapDifficulty { CircleSize = 0 })
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs
new file mode 100644
index 0000000000..1d30ae34cd
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs
@@ -0,0 +1,79 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ public abstract class CatchPlacementBlueprintTestScene : PlacementBlueprintTestScene
+ {
+ protected const double TIME_SNAP = 100;
+
+ protected DrawableCatchHitObject LastObject;
+
+ protected new ScrollingHitObjectContainer HitObjectContainer => contentContainer.Playfield.HitObjectContainer;
+
+ protected override Container Content => contentContainer;
+
+ private readonly CatchEditorTestSceneContainer contentContainer;
+
+ protected CatchPlacementBlueprintTestScene()
+ {
+ base.Content.Add(contentContainer = new CatchEditorTestSceneContainer());
+
+ contentContainer.Playfield.Clock = new FramedClock(new ManualClock());
+ }
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ HitObjectContainer.Clear();
+ ResetPlacement();
+ LastObject = null;
+ });
+
+ protected void AddMoveStep(double time, float x) => AddStep($"move to time={time}, x={x}", () =>
+ {
+ float y = HitObjectContainer.PositionAtTime(time);
+ Vector2 pos = HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
+ InputManager.MoveMouseTo(pos);
+ });
+
+ protected void AddClickStep(MouseButton button) => AddStep($"click {button}", () =>
+ {
+ InputManager.Click(button);
+ });
+
+ protected IEnumerable FruitOutlines => Content.ChildrenOfType();
+
+ // Unused because AddHitObject is overriden
+ protected override Container CreateHitObjectContainer() => new Container();
+
+ protected override void AddHitObject(DrawableHitObject hitObject)
+ {
+ LastObject = (DrawableCatchHitObject)hitObject;
+ contentContainer.Playfield.HitObjectContainer.Add(hitObject);
+ }
+
+ protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
+ {
+ var result = base.SnapForBlueprint(blueprint);
+ result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;
+ return result;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs
new file mode 100644
index 0000000000..dcdc32145b
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ public abstract class CatchSelectionBlueprintTestScene : SelectionBlueprintTestScene
+ {
+ protected ScrollingHitObjectContainer HitObjectContainer => contentContainer.Playfield.HitObjectContainer;
+
+ protected override Container Content => contentContainer;
+
+ private readonly CatchEditorTestSceneContainer contentContainer;
+
+ protected CatchSelectionBlueprintTestScene()
+ {
+ base.Content.Add(contentContainer = new CatchEditorTestSceneContainer());
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs
new file mode 100644
index 0000000000..e3811b7669
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs
@@ -0,0 +1,87 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ public class TestSceneBananaShowerPlacementBlueprint : CatchPlacementBlueprintTestScene
+ {
+ protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableBananaShower((BananaShower)hitObject);
+
+ protected override PlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint();
+
+ protected override void AddHitObject(DrawableHitObject hitObject)
+ {
+ // Create nested bananas (but positions are not randomized because beatmap processing is not done).
+ hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), Beatmap.Value.BeatmapInfo.BaseDifficulty);
+
+ base.AddHitObject(hitObject);
+ }
+
+ [Test]
+ public void TestBasicPlacement()
+ {
+ const double start_time = 100;
+ const double end_time = 500;
+
+ AddMoveStep(start_time, 0);
+ AddClickStep(MouseButton.Left);
+ AddMoveStep(end_time, 0);
+ AddClickStep(MouseButton.Right);
+ AddAssert("banana shower is placed", () => LastObject is DrawableBananaShower);
+ AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time));
+ AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time));
+ }
+
+ [Test]
+ public void TestReversePlacement()
+ {
+ const double start_time = 100;
+ const double end_time = 500;
+
+ AddMoveStep(end_time, 0);
+ AddClickStep(MouseButton.Left);
+ AddMoveStep(start_time, 0);
+ AddClickStep(MouseButton.Right);
+ AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time));
+ AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time));
+ }
+
+ [Test]
+ public void TestFinishWithZeroDuration()
+ {
+ AddMoveStep(100, 0);
+ AddClickStep(MouseButton.Left);
+ AddClickStep(MouseButton.Right);
+ AddAssert("banana shower is not placed", () => LastObject == null);
+ AddAssert("state is waiting", () => CurrentBlueprint?.PlacementActive == PlacementBlueprint.PlacementState.Waiting);
+ }
+
+ [Test]
+ public void TestOpacity()
+ {
+ AddMoveStep(100, 0);
+ AddClickStep(MouseButton.Left);
+ AddUntilStep("outline is semitransparent", () => Precision.DefinitelyBigger(1, timeSpanOutline.Alpha));
+ AddMoveStep(200, 0);
+ AddUntilStep("outline is opaque", () => Precision.AlmostEquals(timeSpanOutline.Alpha, 1));
+ AddMoveStep(100, 0);
+ AddUntilStep("outline is semitransparent", () => Precision.DefinitelyBigger(1, timeSpanOutline.Alpha));
+ }
+
+ private TimeSpanOutline timeSpanOutline => Content.ChildrenOfType().Single();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs
new file mode 100644
index 0000000000..161c685043
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs
@@ -0,0 +1,14 @@
+// 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.Tests.Visual;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ [TestFixture]
+ public class TestSceneEditor : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new CatchRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs
new file mode 100644
index 0000000000..4b1c45ae2f
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs
@@ -0,0 +1,44 @@
+// 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.Utils;
+using osu.Game.Rulesets.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ public class TestSceneFruitPlacementBlueprint : CatchPlacementBlueprintTestScene
+ {
+ protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableFruit((Fruit)hitObject);
+
+ protected override PlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint();
+
+ [Test]
+ public void TestFruitPlacementPosition()
+ {
+ const double time = 300;
+ const float x = CatchPlayfield.CENTER_X;
+
+ AddMoveStep(time, x);
+ AddClickStep(MouseButton.Left);
+
+ AddAssert("outline position is correct", () =>
+ {
+ var outline = FruitOutlines.Single();
+ return Precision.AlmostEquals(outline.X, x) &&
+ Precision.AlmostEquals(outline.Y, HitObjectContainer.PositionAtTime(time));
+ });
+
+ AddAssert("fruit time is correct", () => Precision.AlmostEquals(LastObject.StartTimeBindable.Value, time));
+ AddAssert("fruit position is correct", () => Precision.AlmostEquals(LastObject.X, x));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
new file mode 100644
index 0000000000..1b96175020
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
@@ -0,0 +1,38 @@
+// 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.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ public class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
+ {
+ public TestSceneJuiceStreamSelectionBlueprint()
+ {
+ var hitObject = new JuiceStream
+ {
+ OriginalX = 100,
+ StartTime = 100,
+ Path = new SliderPath(PathType.PerfectCurve, new[]
+ {
+ Vector2.Zero,
+ new Vector2(200, 100),
+ new Vector2(0, 200),
+ }),
+ };
+ var controlPoint = new ControlPointInfo();
+ controlPoint.Add(0, new TimingControlPoint
+ {
+ BeatLength = 100
+ });
+ hitObject.ApplyDefaults(controlPoint, new BeatmapDifficulty { CircleSize = 0 });
+ AddBlueprint(new JuiceStreamSelectionBlueprint(hitObject));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs
new file mode 100644
index 0000000000..5e4b6d9e1a
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs
@@ -0,0 +1,288 @@
+// 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.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ [TestFixture]
+ public class JuiceStreamPathTest
+ {
+ [TestCase(1e3, true, false)]
+ // When the coordinates are large, the slope invariant fails within the specified absolute allowance due to the floating-number precision.
+ [TestCase(1e9, false, false)]
+ // Using discrete values sometimes discover more edge cases.
+ [TestCase(10, true, true)]
+ public void TestRandomInsertSetPosition(double scale, bool checkSlope, bool integralValues)
+ {
+ var rng = new Random(1);
+ var path = new JuiceStreamPath();
+
+ for (int iteration = 0; iteration < 100000; iteration++)
+ {
+ if (rng.Next(10) == 0)
+ path.Clear();
+
+ int vertexCount = path.Vertices.Count;
+
+ switch (rng.Next(2))
+ {
+ case 0:
+ {
+ double distance = rng.NextDouble() * scale * 2 - scale;
+ if (integralValues)
+ distance = Math.Round(distance);
+
+ float oldX = path.PositionAtDistance(distance);
+ int index = path.InsertVertex(distance);
+ Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1));
+ Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance));
+ Assert.That(path.Vertices[index].X, Is.EqualTo(oldX));
+ break;
+ }
+
+ case 1:
+ {
+ int index = rng.Next(path.Vertices.Count);
+ double distance = path.Vertices[index].Distance;
+ float newX = (float)(rng.NextDouble() * scale * 2 - scale);
+ if (integralValues)
+ newX = MathF.Round(newX);
+
+ path.SetVertexPosition(index, newX);
+ Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount));
+ Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance));
+ Assert.That(path.Vertices[index].X, Is.EqualTo(newX));
+ break;
+ }
+ }
+
+ assertInvariants(path.Vertices, checkSlope);
+ }
+ }
+
+ [Test]
+ public void TestRemoveVertices()
+ {
+ var path = new JuiceStreamPath();
+ path.Add(10, 5);
+ path.Add(20, -5);
+
+ int removeCount = path.RemoveVertices((v, i) => v.Distance == 10 && i == 1);
+ Assert.That(removeCount, Is.EqualTo(1));
+ Assert.That(path.Vertices, Is.EqualTo(new[]
+ {
+ new JuiceStreamPathVertex(0, 0),
+ new JuiceStreamPathVertex(20, -5)
+ }));
+
+ removeCount = path.RemoveVertices((_, i) => i == 0);
+ Assert.That(removeCount, Is.EqualTo(1));
+ Assert.That(path.Vertices, Is.EqualTo(new[]
+ {
+ new JuiceStreamPathVertex(20, -5)
+ }));
+
+ removeCount = path.RemoveVertices((_, i) => true);
+ Assert.That(removeCount, Is.EqualTo(1));
+ Assert.That(path.Vertices, Is.EqualTo(new[]
+ {
+ new JuiceStreamPathVertex()
+ }));
+ }
+
+ [Test]
+ public void TestResampleVertices()
+ {
+ var path = new JuiceStreamPath();
+ path.Add(-100, -10);
+ path.Add(100, 50);
+ path.ResampleVertices(new double[]
+ {
+ -50,
+ 0,
+ 70,
+ 120
+ });
+ Assert.That(path.Vertices, Is.EqualTo(new[]
+ {
+ new JuiceStreamPathVertex(-100, -10),
+ new JuiceStreamPathVertex(-50, -5),
+ new JuiceStreamPathVertex(0, 0),
+ new JuiceStreamPathVertex(70, 35),
+ new JuiceStreamPathVertex(100, 50),
+ new JuiceStreamPathVertex(100, 50),
+ }));
+
+ path.Clear();
+ path.SetVertexPosition(0, 10);
+ path.ResampleVertices(Array.Empty());
+ Assert.That(path.Vertices, Is.EqualTo(new[]
+ {
+ new JuiceStreamPathVertex(0, 10)
+ }));
+ }
+
+ [Test]
+ public void TestRandomConvertFromSliderPath()
+ {
+ var rng = new Random(1);
+ var path = new JuiceStreamPath();
+ var sliderPath = new SliderPath();
+
+ for (int iteration = 0; iteration < 10000; iteration++)
+ {
+ sliderPath.ControlPoints.Clear();
+
+ do
+ {
+ int start = sliderPath.ControlPoints.Count;
+
+ do
+ {
+ float x = (float)(rng.NextDouble() * 1e3);
+ float y = (float)(rng.NextDouble() * 1e3);
+ sliderPath.ControlPoints.Add(new PathControlPoint(new Vector2(x, y)));
+ } while (rng.Next(2) != 0);
+
+ int length = sliderPath.ControlPoints.Count - start + 1;
+ sliderPath.ControlPoints[start].Type.Value = length <= 2 ? PathType.Linear : length == 3 ? PathType.PerfectCurve : PathType.Bezier;
+ } while (rng.Next(3) != 0);
+
+ if (rng.Next(5) == 0)
+ sliderPath.ExpectedDistance.Value = rng.NextDouble() * 3e3;
+ else
+ sliderPath.ExpectedDistance.Value = null;
+
+ path.ConvertFromSliderPath(sliderPath);
+ Assert.That(path.Vertices[0].Distance, Is.EqualTo(0));
+ Assert.That(path.Distance, Is.EqualTo(sliderPath.Distance).Within(1e-3));
+ assertInvariants(path.Vertices, true);
+
+ double[] sampleDistances = Enumerable.Range(0, 10)
+ .Select(_ => rng.NextDouble() * sliderPath.Distance)
+ .ToArray();
+
+ foreach (double distance in sampleDistances)
+ {
+ float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
+ Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
+ }
+
+ path.ResampleVertices(sampleDistances);
+ assertInvariants(path.Vertices, true);
+
+ foreach (double distance in sampleDistances)
+ {
+ float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
+ Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
+ }
+ }
+ }
+
+ [Test]
+ public void TestRandomConvertToSliderPath()
+ {
+ var rng = new Random(1);
+ var path = new JuiceStreamPath();
+ var sliderPath = new SliderPath();
+
+ for (int iteration = 0; iteration < 10000; iteration++)
+ {
+ path.Clear();
+
+ do
+ {
+ double distance = rng.NextDouble() * 1e3;
+ float x = (float)(rng.NextDouble() * 1e3);
+ path.Add(distance, x);
+ } while (rng.Next(5) != 0);
+
+ float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT);
+
+ path.ConvertToSliderPath(sliderPath, sliderStartY);
+ Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3));
+ Assert.That(sliderPath.ControlPoints[0].Position.Value.X, Is.EqualTo(path.Vertices[0].X));
+ assertInvariants(path.Vertices, true);
+
+ foreach (var point in sliderPath.ControlPoints)
+ {
+ Assert.That(point.Type.Value, Is.EqualTo(PathType.Linear).Or.Null);
+ Assert.That(sliderStartY + point.Position.Value.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
+ }
+
+ for (int i = 0; i < 10; i++)
+ {
+ double distance = rng.NextDouble() * path.Distance;
+ float expected = path.PositionAtDistance(distance);
+ Assert.That(sliderPath.PositionAt(distance / sliderPath.Distance).X, Is.EqualTo(expected).Within(1e-3));
+ }
+ }
+ }
+
+ [Test]
+ public void TestInvalidation()
+ {
+ var path = new JuiceStreamPath();
+ Assert.That(path.InvalidationID, Is.EqualTo(1));
+ int previousId = path.InvalidationID;
+
+ path.InsertVertex(10);
+ checkNewId();
+
+ path.SetVertexPosition(1, 5);
+ checkNewId();
+
+ path.Add(20, 0);
+ checkNewId();
+
+ path.RemoveVertices((v, _) => v.Distance == 20);
+ checkNewId();
+
+ path.ResampleVertices(new double[] { 5, 10, 15 });
+ checkNewId();
+
+ path.Clear();
+ checkNewId();
+
+ path.ConvertFromSliderPath(new SliderPath());
+ checkNewId();
+
+ void checkNewId()
+ {
+ Assert.That(path.InvalidationID, Is.Not.EqualTo(previousId));
+ previousId = path.InvalidationID;
+ }
+ }
+
+ private void assertInvariants(IReadOnlyList vertices, bool checkSlope)
+ {
+ Assert.That(vertices, Is.Not.Empty);
+
+ for (int i = 0; i < vertices.Count; i++)
+ {
+ Assert.That(double.IsFinite(vertices[i].Distance));
+ Assert.That(float.IsFinite(vertices[i].X));
+ }
+
+ for (int i = 1; i < vertices.Count; i++)
+ {
+ Assert.That(vertices[i].Distance, Is.GreaterThanOrEqualTo(vertices[i - 1].Distance));
+
+ if (!checkSlope) continue;
+
+ float xDiff = Math.Abs(vertices[i].X - vertices[i - 1].X);
+ double distanceDiff = vertices[i].Distance - vertices[i - 1].Distance;
+ Assert.That(xDiff, Is.LessThanOrEqualTo(distanceDiff).Within(Precision.FLOAT_EPSILON));
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini
new file mode 100644
index 0000000000..94c6b5b58d
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini
@@ -0,0 +1,2 @@
+[General]
+// no version specified means v1
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
index 1248409b2a..09362929d2 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
@@ -4,10 +4,8 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
-using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
@@ -21,12 +19,6 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneCatchModHidden : ModTestScene
{
- [BackgroundDependencyLoader]
- private void load()
- {
- LocalConfig.SetValue(OsuSetting.IncreaseFirstObjectVisibility, false);
- }
-
[Test]
public void TestJuiceStream()
{
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs
index 64695153b5..b7cd6737b1 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs
@@ -1,8 +1,16 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Linq;
using NUnit.Framework;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Screens;
+using osu.Framework.Testing;
+using osu.Game.Screens.Play.HUD;
+using osu.Game.Skinning;
using osu.Game.Tests.Visual;
+using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
@@ -10,5 +18,22 @@ namespace osu.Game.Rulesets.Catch.Tests
public class TestSceneCatchPlayerLegacySkin : LegacySkinPlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+
+ [Test]
+ public void TestLegacyHUDComboCounterHidden([Values] bool withModifiedSkin)
+ {
+ if (withModifiedSkin)
+ {
+ AddStep("change component scale", () => Player.ChildrenOfType().First().Scale = new Vector2(2f));
+ AddStep("update target", () => Player.ChildrenOfType().ForEach(LegacySkin.UpdateDrawableTarget));
+ AddStep("exit player", () => Player.Exit());
+ CreateTest(null);
+ }
+
+ AddAssert("legacy HUD combo counter hidden", () =>
+ {
+ return Player.ChildrenOfType().All(c => c.ChildrenOfType().Single().Alpha == 0f);
+ });
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
new file mode 100644
index 0000000000..ec186bcfb2
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
@@ -0,0 +1,114 @@
+// 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.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.Judgements;
+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 Direction = osu.Game.Rulesets.Catch.UI.Direction;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneCatchSkinConfiguration : OsuTestScene
+ {
+ [Cached]
+ private readonly DroppedObjectContainer droppedObjectContainer;
+
+ private Catcher catcher;
+
+ private readonly Container container;
+
+ public TestSceneCatchSkinConfiguration()
+ {
+ Add(droppedObjectContainer = new DroppedObjectContainer());
+ Add(container = new Container { RelativeSizeAxes = Axes.Both });
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestCatcherPlateFlipping(bool flip)
+ {
+ AddStep("setup catcher", () =>
+ {
+ var skin = new TestSkin { FlipCatcherPlate = flip };
+ container.Child = new SkinProvidingContainer(skin)
+ {
+ Child = catcher = new Catcher(new Container())
+ {
+ Anchor = Anchor.Centre
+ }
+ };
+ });
+
+ Fruit fruit = new Fruit();
+
+ AddStep("catch fruit", () => catchFruit(fruit, 20));
+
+ float position = 0;
+
+ AddStep("record fruit position", () => position = getCaughtObjectPosition(fruit));
+
+ AddStep("face left", () => catcher.VisualDirection = Direction.Left);
+
+ if (flip)
+ AddAssert("fruit position changed", () => !Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
+ else
+ AddAssert("fruit position unchanged", () => Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
+
+ AddStep("face right", () => catcher.VisualDirection = Direction.Right);
+
+ AddAssert("fruit position restored", () => Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
+ }
+
+ private float getCaughtObjectPosition(Fruit fruit)
+ {
+ var caughtObject = catcher.ChildrenOfType().Single(c => c.HitObject == fruit);
+ return caughtObject.Parent.ToSpaceOfOtherDrawable(caughtObject.Position, catcher).X;
+ }
+
+ private void catchFruit(Fruit fruit, float x)
+ {
+ fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ var drawableFruit = new DrawableFruit(fruit) { X = x };
+ var judgement = fruit.CreateJudgement();
+ catcher.OnNewResult(drawableFruit, new CatchJudgementResult(fruit, judgement)
+ {
+ Type = judgement.MaxResult
+ });
+ }
+
+ private class TestSkin : DefaultSkin
+ {
+ public bool FlipCatcherPlate { get; set; }
+
+ public TestSkin()
+ : base(null)
+ {
+ }
+
+ public override IBindable GetConfig(TLookup lookup)
+ {
+ if (lookup is CatchSkinConfiguration config)
+ {
+ if (config == CatchSkinConfiguration.FlipCatcherPlate)
+ return SkinUtils.As(new Bindable(FlipCatcherPlate));
+ }
+
+ return base.GetConfig(lookup);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index 517027a9fc..0a2dff6a21 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -6,8 +6,8 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
-using osu.Game.Rulesets.Catch.UI;
using osu.Framework.Graphics;
+using osu.Game.Rulesets.Catch.UI;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
@@ -31,10 +31,10 @@ namespace osu.Game.Rulesets.Catch.Tests
[Resolved]
private OsuConfigManager config { get; set; }
- private Container droppedObjectContainer;
-
private TestCatcher catcher;
+ private DroppedObjectContainer droppedObjectContainer;
+
[SetUp]
public void SetUp() => Schedule(() =>
{
@@ -43,19 +43,24 @@ namespace osu.Game.Rulesets.Catch.Tests
CircleSize = 0,
};
- var trailContainer = new Container();
- droppedObjectContainer = new Container();
- catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty);
-
- Child = new Container
+ var trailContainer = new Container
{
Anchor = Anchor.Centre,
+ };
+ droppedObjectContainer = new DroppedObjectContainer();
+ Child = new DependencyProvidingContainer
+ {
+ CachedDependencies = new (Type, object)[]
+ {
+ (typeof(DroppedObjectContainer), droppedObjectContainer),
+ },
Children = new Drawable[]
{
- trailContainer,
droppedObjectContainer,
- catcher
- }
+ catcher = new TestCatcher(trailContainer, difficulty),
+ trailContainer
+ },
+ Anchor = Anchor.Centre
};
});
@@ -188,9 +193,9 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9));
checkPlate(10);
AddAssert("caught objects are stacked", () =>
- catcher.CaughtObjects.All(obj => obj.Y <= Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
- catcher.CaughtObjects.Any(obj => obj.Y == Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
- catcher.CaughtObjects.Any(obj => obj.Y < -25));
+ catcher.CaughtObjects.All(obj => obj.Y <= 0) &&
+ catcher.CaughtObjects.Any(obj => obj.Y == 0) &&
+ catcher.CaughtObjects.Any(obj => obj.Y < 0));
}
[Test]
@@ -216,7 +221,7 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddAssert("correct hit lighting colour", () =>
- catcher.ChildrenOfType().First()?.ObjectColour == fruitColour);
+ catcher.ChildrenOfType().First()?.Entry?.ObjectColour == fruitColour);
}
[Test]
@@ -293,8 +298,8 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public IEnumerable CaughtObjects => this.ChildrenOfType();
- public TestCatcher(Container trailsTarget, Container droppedObjectTarget, BeatmapDifficulty difficulty)
- : base(trailsTarget, droppedObjectTarget, difficulty)
+ public TestCatcher(Container trailsTarget, BeatmapDifficulty difficulty)
+ : base(trailsTarget, difficulty)
{
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index ad404e1f63..877e115e2f 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -6,7 +6,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Framework.Utils;
@@ -95,20 +94,14 @@ namespace osu.Game.Rulesets.Catch.Tests
CircleSize = circleSize
};
- SetContents(() =>
+ SetContents(_ =>
{
- var droppedObjectContainer = new Container
- {
- RelativeSizeAxes = Axes.Both
- };
-
return new CatchInputManager(catchRuleset)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
- droppedObjectContainer,
- new TestCatcherArea(droppedObjectContainer, beatmapDifficulty)
+ new TestCatcherArea(beatmapDifficulty)
{
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
@@ -126,9 +119,13 @@ namespace osu.Game.Rulesets.Catch.Tests
private class TestCatcherArea : CatcherArea
{
- public TestCatcherArea(Container droppedObjectContainer, BeatmapDifficulty beatmapDifficulty)
- : base(droppedObjectContainer, beatmapDifficulty)
+ [Cached]
+ private readonly DroppedObjectContainer droppedObjectContainer;
+
+ public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
+ : base(beatmapDifficulty)
{
+ AddInternal(droppedObjectContainer = new DroppedObjectContainer());
}
public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperDashState(status ? 2 : 1);
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
index c7b322c8a0..064a84cb98 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
scoreProcessor = new ScoreProcessor();
- SetContents(() => new CatchComboDisplay
+ SetContents(_ => new CatchComboDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
index 3e4995482d..fd6a9c7b7b 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
@@ -174,8 +174,8 @@ namespace osu.Game.Rulesets.Catch.Tests
private void addToPlayfield(DrawableCatchHitObject drawable)
{
- foreach (var mod in SelectedMods.Value.OfType())
- mod.ApplyToDrawableHitObjects(new[] { drawable });
+ foreach (var mod in SelectedMods.Value.OfType())
+ mod.ApplyToDrawableHitObject(drawable);
drawableRuleset.Playfield.Add(drawable);
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
index 3a651605d3..943adbef52 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
@@ -19,22 +19,22 @@ namespace osu.Game.Rulesets.Catch.Tests
{
base.LoadComplete();
- AddStep("show pear", () => SetContents(() => createDrawableFruit(0)));
- AddStep("show grape", () => SetContents(() => createDrawableFruit(1)));
- AddStep("show pineapple / apple", () => SetContents(() => createDrawableFruit(2)));
- AddStep("show raspberry / orange", () => SetContents(() => createDrawableFruit(3)));
+ AddStep("show pear", () => SetContents(_ => createDrawableFruit(0)));
+ AddStep("show grape", () => SetContents(_ => createDrawableFruit(1)));
+ AddStep("show pineapple / apple", () => SetContents(_ => createDrawableFruit(2)));
+ AddStep("show raspberry / orange", () => SetContents(_ => createDrawableFruit(3)));
- AddStep("show banana", () => SetContents(createDrawableBanana));
+ AddStep("show banana", () => SetContents(_ => createDrawableBanana()));
- AddStep("show droplet", () => SetContents(() => createDrawableDroplet()));
- AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet));
+ AddStep("show droplet", () => SetContents(_ => createDrawableDroplet()));
+ AddStep("show tiny droplet", () => SetContents(_ => createDrawableTinyDroplet()));
- AddStep("show hyperdash pear", () => SetContents(() => createDrawableFruit(0, true)));
- AddStep("show hyperdash grape", () => SetContents(() => createDrawableFruit(1, true)));
- AddStep("show hyperdash pineapple / apple", () => SetContents(() => createDrawableFruit(2, true)));
- AddStep("show hyperdash raspberry / orange", () => SetContents(() => createDrawableFruit(3, true)));
+ AddStep("show hyperdash pear", () => SetContents(_ => createDrawableFruit(0, true)));
+ AddStep("show hyperdash grape", () => SetContents(_ => createDrawableFruit(1, true)));
+ AddStep("show hyperdash pineapple / apple", () => SetContents(_ => createDrawableFruit(2, true)));
+ AddStep("show hyperdash raspberry / orange", () => SetContents(_ => createDrawableFruit(3, true)));
- AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true)));
+ AddStep("show hyperdash droplet", () => SetContents(_ => createDrawableDroplet(true)));
}
private Drawable createDrawableFruit(int indexInBeatmap, bool hyperdash = false) =>
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs
index 125e0c674c..9446e864a1 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Tests
protected override void LoadComplete()
{
- AddStep("fruit changes visual and hyper", () => SetContents(() => new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit
+ AddStep("fruit changes visual and hyper", () => SetContents(_ => new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit
{
IndexInBeatmapBindable = { BindTarget = indexInBeatmap },
HyperDashBindable = { BindTarget = hyperDash },
}))));
- AddStep("droplet changes hyper", () => SetContents(() => new TestDrawableCatchHitObjectSpecimen(new DrawableDroplet(new Droplet
+ AddStep("droplet changes hyper", () => SetContents(_ => new TestDrawableCatchHitObjectSpecimen(new DrawableDroplet(new Droplet
{
HyperDashBindable = { BindTarget = hyperDash },
}))));
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
index 683a776dcc..e7b0259ea2 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
@@ -118,11 +118,10 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("create hyper-dashing catcher", () =>
{
- Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container())
+ Child = setupSkinHierarchy(catcherArea = new TestCatcherArea
{
Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Scale = new Vector2(4f),
+ Origin = Anchor.Centre
}, skin);
});
@@ -139,7 +138,7 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("finish hyper-dashing", () =>
{
- catcherArea.MovableCatcher.SetHyperDashState(1);
+ catcherArea.MovableCatcher.SetHyperDashState();
catcherArea.MovableCatcher.FinishTransforms();
});
@@ -206,5 +205,18 @@ namespace osu.Game.Rulesets.Catch.Tests
{
}
}
+
+ private class TestCatcherArea : CatcherArea
+ {
+ [Cached]
+ private readonly DroppedObjectContainer droppedObjectContainer;
+
+ public TestCatcherArea()
+ {
+ Scale = new Vector2(4f);
+
+ AddInternal(droppedObjectContainer = new DroppedObjectContainer());
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs
index eea83ef7c1..bc3daca16f 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs
@@ -32,28 +32,28 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(true, false)]
[TestCase(false, true)]
[TestCase(false, false)]
- public override void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin)
+ public void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin)
{
- TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
- base.TestBeatmapComboColours(userHasCustomColours, useBeatmapSkin);
+ PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
+ ConfigureTest(useBeatmapSkin, true, userHasCustomColours);
AddAssert("is beatmap skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours));
}
[TestCase(true)]
[TestCase(false)]
- public override void TestBeatmapComboColoursOverride(bool useBeatmapSkin)
+ public void TestBeatmapComboColoursOverride(bool useBeatmapSkin)
{
- TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
- base.TestBeatmapComboColoursOverride(useBeatmapSkin);
+ PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
+ ConfigureTest(useBeatmapSkin, false, true);
AddAssert("is user custom skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours));
}
[TestCase(true)]
[TestCase(false)]
- public override void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin)
+ public void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin)
{
- TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
- base.TestBeatmapComboColoursOverrideWithDefaultColours(useBeatmapSkin);
+ PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
+ ConfigureTest(useBeatmapSkin, false, false);
AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours));
}
@@ -61,10 +61,10 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(false, true)]
[TestCase(true, false)]
[TestCase(false, false)]
- public override void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour)
+ public void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour)
{
- TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, false);
- base.TestBeatmapNoComboColours(useBeatmapSkin, useBeatmapColour);
+ PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, false));
+ ConfigureTest(useBeatmapSkin, useBeatmapColour, false);
AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours));
}
@@ -72,10 +72,10 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(false, true)]
[TestCase(true, false)]
[TestCase(false, false)]
- public override void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour)
+ public void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour)
{
- TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, false);
- base.TestBeatmapNoComboColoursSkinOverride(useBeatmapSkin, useBeatmapColour);
+ PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, false));
+ ConfigureTest(useBeatmapSkin, useBeatmapColour, true);
AddAssert("is custom user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours));
}
@@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(false)]
public void TestBeatmapHyperDashColours(bool useBeatmapSkin)
{
- TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
+ PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
ConfigureTest(useBeatmapSkin, true, true);
AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == TestBeatmapSkin.HYPER_DASH_COLOUR);
AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == TestBeatmapSkin.HYPER_DASH_AFTER_IMAGE_COLOUR);
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase(false)]
public void TestBeatmapHyperDashColoursOverride(bool useBeatmapSkin)
{
- TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true);
+ PrepareBeatmap(() => new CatchCustomSkinWorkingBeatmap(audio, true));
ConfigureTest(useBeatmapSkin, false, true);
AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == TestSkin.HYPER_DASH_COLOUR);
AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == TestSkin.HYPER_DASH_AFTER_IMAGE_COLOUR);
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 77e9d672e3..484da8e22e 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -2,9 +2,9 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
index 34964fc4ae..7774a7da09 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
@@ -23,7 +23,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
protected override IEnumerable ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken)
{
- var positionData = obj as IHasXPosition;
+ var xPositionData = obj as IHasXPosition;
+ var yPositionData = obj as IHasYPosition;
var comboData = obj as IHasCombo;
switch (obj)
@@ -36,10 +37,11 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
Path = curveData.Path,
NodeSamples = curveData.NodeSamples,
RepeatCount = curveData.RepeatCount,
- X = positionData?.X ?? 0,
+ X = xPositionData?.X ?? 0,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
- LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0
+ LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
+ LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y
}.Yield();
case IHasDuration endTime:
@@ -59,7 +61,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
Samples = obj.Samples,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
- X = positionData?.X ?? 0
+ X = xPositionData?.X ?? 0,
+ LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y
}.Yield();
}
}
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index fac5d03833..3a5322ce82 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -8,7 +8,6 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.MathUtils;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
-using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Beatmaps
@@ -17,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
public const int RNG_SEED = 1337;
+ public bool HardRockOffsets { get; set; }
+
public CatchBeatmapProcessor(IBeatmap beatmap)
: base(beatmap)
{
@@ -43,11 +44,10 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
}
}
- public static void ApplyPositionOffsets(IBeatmap beatmap, params Mod[] mods)
+ public void ApplyPositionOffsets(IBeatmap beatmap)
{
var rng = new FastRandom(RNG_SEED);
- bool shouldApplyHardRockOffset = mods.Any(m => m is ModHardRock);
float? lastPosition = null;
double lastStartTime = 0;
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
switch (obj)
{
case Fruit fruit:
- if (shouldApplyHardRockOffset)
+ if (HardRockOffsets)
applyHardRockOffset(fruit, ref lastPosition, ref lastStartTime, rng);
break;
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index e3c457693e..76863acc78 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -22,7 +22,9 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System;
using osu.Framework.Extensions.EnumExtensions;
+using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Skinning.Legacy;
+using osu.Game.Rulesets.Edit;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
@@ -161,13 +163,13 @@ namespace osu.Game.Rulesets.Catch
switch (result)
{
case HitResult.LargeTickHit:
- return "large droplet";
+ return "Large droplet";
case HitResult.SmallTickHit:
- return "small droplet";
+ return "Small droplet";
case HitResult.LargeBonus:
- return "banana";
+ return "Banana";
}
return base.GetDisplayNameForHitResult(result);
@@ -175,12 +177,14 @@ namespace osu.Game.Rulesets.Catch
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
- public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);
+ public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new CatchLegacySkinTransformer(skin);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score);
public int LegacyID => 2;
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
+
+ public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
}
}
diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
index 668f7197be..e736d68740 100644
--- a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
+++ b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
@@ -8,9 +8,7 @@ namespace osu.Game.Rulesets.Catch
Fruit,
Banana,
Droplet,
- CatcherIdle,
- CatcherFail,
- CatcherKiai,
+ Catcher,
CatchComboCounter
}
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
index fa9011d826..4e05b1e3e0 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
@@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchDifficultyAttributes : DifficultyAttributes
{
- public double ApproachRate;
+ public double ApproachRate { get; set; }
}
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index f5cce47186..9feaa55051 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
}
}
- protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
{
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
@@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
return new Skill[]
{
- new Movement(mods, halfCatcherWidth),
+ new Movement(mods, halfCatcherWidth, clockRate),
};
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
index 6a3a16ed33..439890dac2 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Extensions;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@@ -33,15 +32,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
mods = Score.Mods;
- fruitsHit = Score.Statistics.GetOrDefault(HitResult.Great);
- 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))
- return 0;
+ fruitsHit = Score.Statistics.GetValueOrDefault(HitResult.Great);
+ ticksHit = Score.Statistics.GetValueOrDefault(HitResult.LargeTickHit);
+ tinyTicksHit = Score.Statistics.GetValueOrDefault(HitResult.SmallTickHit);
+ tinyTicksMissed = Score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss);
+ misses = Score.Statistics.GetValueOrDefault(HitResult.Miss);
// We are heavily relying on aim in catch the beat
double value = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
index d936ef97ac..e19098c580 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
@@ -24,8 +24,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
///
public readonly double StrainTime;
- public readonly double ClockRate;
-
public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth)
: base(hitObject, lastObject, clockRate)
{
@@ -37,7 +35,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
// 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 75e17f6c48..4372ed938c 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -28,10 +28,21 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
private float lastDistanceMoved;
private double lastStrainTime;
- public Movement(Mod[] mods, float halfCatcherWidth)
+ ///
+ /// The speed multiplier applied to the player's catcher.
+ ///
+ private readonly double catcherSpeedMultiplier;
+
+ public Movement(Mod[] mods, float halfCatcherWidth, double clockRate)
: base(mods)
{
HalfCatcherWidth = halfCatcherWidth;
+
+ // In catch, clockrate adjustments do not only affect the timings of hitobjects,
+ // but also the speed of the player's catcher, which has an impact on difficulty
+ // TODO: Support variable clockrates caused by mods such as ModTimeRamp
+ // (perhaps by using IApplicableToRate within the CatchDifficultyHitObject constructor to set a catcher speed for each object before processing)
+ catcherSpeedMultiplier = clockRate;
}
protected override double StrainValueOf(DifficultyHitObject current)
@@ -48,7 +59,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
float distanceMoved = playerPosition - lastPlayerPosition.Value;
- double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catchCurrent.ClockRate);
+ double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
double sqrtStrain = Math.Sqrt(weightedStrainTime);
@@ -81,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
playerPosition = catchCurrent.NormalizedPosition;
}
- 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
+ distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
}
lastPlayerPosition = playerPosition;
diff --git a/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs
new file mode 100644
index 0000000000..31075db7d1
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.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.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class BananaShowerCompositionTool : HitObjectCompositionTool
+ {
+ public BananaShowerCompositionTool()
+ : base(nameof(BananaShower))
+ {
+ }
+
+ public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
+
+ public override PlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs
new file mode 100644
index 0000000000..6dea8b0712
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.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 osu.Framework.Input.Events;
+using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class BananaShowerPlacementBlueprint : CatchPlacementBlueprint
+ {
+ private readonly TimeSpanOutline outline;
+
+ public BananaShowerPlacementBlueprint()
+ {
+ InternalChild = outline = new TimeSpanOutline();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ outline.UpdateFrom(HitObjectContainer, HitObject);
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ switch (PlacementActive)
+ {
+ case PlacementState.Waiting:
+ if (e.Button != MouseButton.Left) break;
+
+ BeginPlacement(true);
+ return true;
+
+ case PlacementState.Active:
+ if (e.Button != MouseButton.Right) break;
+
+ // If the duration is negative, swap the start and the end time to make the duration positive.
+ if (HitObject.Duration < 0)
+ {
+ HitObject.StartTime = HitObject.EndTime;
+ HitObject.Duration = -HitObject.Duration;
+ }
+
+ EndPlacement(HitObject.Duration > 0);
+ return true;
+ }
+
+ return base.OnMouseDown(e);
+ }
+
+ public override void UpdateTimeAndPosition(SnapResult result)
+ {
+ base.UpdateTimeAndPosition(result);
+
+ if (!(result.Time is double time)) return;
+
+ switch (PlacementActive)
+ {
+ case PlacementState.Waiting:
+ HitObject.StartTime = time;
+ break;
+
+ case PlacementState.Active:
+ HitObject.EndTime = time;
+ break;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs
new file mode 100644
index 0000000000..9132b1a9e8
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs
@@ -0,0 +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 osu.Game.Rulesets.Catch.Objects;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class BananaShowerSelectionBlueprint : CatchSelectionBlueprint
+ {
+ public BananaShowerSelectionBlueprint(BananaShower hitObject)
+ : base(hitObject)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs
new file mode 100644
index 0000000000..5a32d241ad
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.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.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class CatchPlacementBlueprint : PlacementBlueprint
+ where THitObject : CatchHitObject, new()
+ {
+ protected new THitObject HitObject => (THitObject)base.HitObject;
+
+ protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
+
+ [Resolved]
+ private Playfield playfield { get; set; }
+
+ public CatchPlacementBlueprint()
+ : base(new THitObject())
+ {
+ }
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs
new file mode 100644
index 0000000000..7e566c810c
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs
@@ -0,0 +1,39 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public abstract class CatchSelectionBlueprint : HitObjectSelectionBlueprint
+ where THitObject : CatchHitObject
+ {
+ protected override bool AlwaysShowWhenSelected => true;
+
+ public override Vector2 ScreenSpaceSelectionPoint
+ {
+ get
+ {
+ Vector2 position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
+ return HitObjectContainer.ToScreenSpace(position + new Vector2(0, HitObjectContainer.DrawHeight));
+ }
+ }
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SelectionQuad.Contains(screenSpacePos);
+
+ protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
+
+ [Resolved]
+ private Playfield playfield { get; set; }
+
+ protected CatchSelectionBlueprint(THitObject hitObject)
+ : base(hitObject)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs
new file mode 100644
index 0000000000..0c03068e26
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs
@@ -0,0 +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.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Skinning.Default;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
+{
+ public class FruitOutline : CompositeDrawable
+ {
+ public FruitOutline()
+ {
+ Anchor = Anchor.BottomLeft;
+ Origin = Anchor.Centre;
+ InternalChild = new BorderPiece();
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour osuColour)
+ {
+ Colour = osuColour.Yellow;
+ }
+
+ public void UpdateFrom(CatchHitObject hitObject)
+ {
+ Scale = new Vector2(hitObject.Scale);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/NestedOutlineContainer.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/NestedOutlineContainer.cs
new file mode 100644
index 0000000000..cf916b27a4
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/NestedOutlineContainer.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 System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.UI.Scrolling;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
+{
+ public class NestedOutlineContainer : CompositeDrawable
+ {
+ private readonly List nestedHitObjects = new List();
+
+ public NestedOutlineContainer()
+ {
+ Anchor = Anchor.BottomLeft;
+ }
+
+ public void UpdateNestedObjectsFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
+ {
+ nestedHitObjects.Clear();
+ nestedHitObjects.AddRange(parentHitObject.NestedHitObjects
+ .OfType()
+ .Where(h => !(h is TinyDroplet)));
+
+ while (nestedHitObjects.Count < InternalChildren.Count)
+ RemoveInternal(InternalChildren[^1]);
+
+ while (InternalChildren.Count < nestedHitObjects.Count)
+ AddInternal(new FruitOutline());
+
+ for (int i = 0; i < nestedHitObjects.Count; i++)
+ {
+ var hitObject = nestedHitObjects[i];
+ var outline = (FruitOutline)InternalChildren[i];
+ outline.Position = CatchHitObjectUtils.GetStartPosition(hitObjectContainer, hitObject) - Position;
+ outline.UpdateFrom(hitObject);
+ outline.Scale *= hitObject is Droplet ? 0.5f : 1;
+ }
+ }
+
+ protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs
new file mode 100644
index 0000000000..109bf61ea5
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs
@@ -0,0 +1,77 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Lines;
+using osu.Framework.Graphics.Primitives;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
+{
+ public class ScrollingPath : CompositeDrawable
+ {
+ private readonly Path drawablePath;
+
+ private readonly List<(double Distance, float X)> vertices = new List<(double, float)>();
+
+ public ScrollingPath()
+ {
+ Anchor = Anchor.BottomLeft;
+
+ InternalChildren = new Drawable[]
+ {
+ drawablePath = new SmoothPath
+ {
+ PathRadius = 2,
+ Alpha = 0.5f
+ },
+ };
+ }
+
+ public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
+ {
+ double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
+
+ computeDistanceXs(hitObject);
+ drawablePath.Vertices = vertices
+ .Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor)))
+ .ToArray();
+ drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
+ }
+
+ private void computeDistanceXs(JuiceStream hitObject)
+ {
+ vertices.Clear();
+
+ var sliderVertices = new List();
+ hitObject.Path.GetPathToProgress(sliderVertices, 0, 1);
+
+ if (sliderVertices.Count == 0)
+ return;
+
+ double distance = 0;
+ Vector2 lastPosition = Vector2.Zero;
+
+ for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
+ {
+ foreach (var position in sliderVertices)
+ {
+ distance += Vector2.Distance(lastPosition, position);
+ lastPosition = position;
+
+ vertices.Add((distance, position.X));
+ }
+
+ sliderVertices.Reverse();
+ }
+ }
+
+ // Because this has 0x0 size, the contents are otherwise masked away if the start position is outside the screen.
+ protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs
new file mode 100644
index 0000000000..65dfce0493
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs
@@ -0,0 +1,63 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
+{
+ public class TimeSpanOutline : CompositeDrawable
+ {
+ private const float border_width = 4;
+
+ private const float opacity_when_empty = 0.5f;
+
+ private bool isEmpty = true;
+
+ public TimeSpanOutline()
+ {
+ Anchor = Origin = Anchor.BottomLeft;
+ RelativeSizeAxes = Axes.X;
+
+ Masking = true;
+ BorderThickness = border_width;
+ Alpha = opacity_when_empty;
+
+ // A box is needed to make the border visible.
+ InternalChild = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Transparent
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour osuColour)
+ {
+ BorderColour = osuColour.Yellow;
+ }
+
+ public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, BananaShower hitObject)
+ {
+ float startY = hitObjectContainer.PositionAtTime(hitObject.StartTime);
+ float endY = hitObjectContainer.PositionAtTime(hitObject.EndTime);
+
+ Y = Math.Max(startY, endY);
+ float height = Math.Abs(startY - endY);
+
+ bool wasEmpty = isEmpty;
+ isEmpty = height == 0;
+ if (wasEmpty != isEmpty)
+ this.FadeTo(isEmpty ? opacity_when_empty : 1f, 150);
+
+ Height = Math.Max(height, border_width);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs
new file mode 100644
index 0000000000..e169e3b75c
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.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 osu.Framework.Input.Events;
+using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class FruitPlacementBlueprint : CatchPlacementBlueprint
+ {
+ private readonly FruitOutline outline;
+
+ public FruitPlacementBlueprint()
+ {
+ InternalChild = outline = new FruitOutline();
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ BeginPlacement();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ outline.Position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
+ outline.UpdateFrom(HitObject);
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ if (e.Button != MouseButton.Left) return base.OnMouseDown(e);
+
+ EndPlacement(true);
+ return true;
+ }
+
+ public override void UpdateTimeAndPosition(SnapResult result)
+ {
+ base.UpdateTimeAndPosition(result);
+
+ HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs
new file mode 100644
index 0000000000..150297badb
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.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.Game.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class FruitSelectionBlueprint : CatchSelectionBlueprint
+ {
+ private readonly FruitOutline outline;
+
+ public FruitSelectionBlueprint(Fruit hitObject)
+ : base(hitObject)
+ {
+ InternalChild = outline = new FruitOutline();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!IsSelected) return;
+
+ outline.Position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
+ outline.UpdateFrom(HitObject);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
new file mode 100644
index 0000000000..0614c4c24d
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
@@ -0,0 +1,91 @@
+// 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.Allocation;
+using osu.Framework.Caching;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Primitives;
+using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class JuiceStreamSelectionBlueprint : CatchSelectionBlueprint
+ {
+ public override Quad SelectionQuad => HitObjectContainer.ToScreenSpace(getBoundingBox().Offset(new Vector2(0, HitObjectContainer.DrawHeight)));
+
+ private float minNestedX;
+ private float maxNestedX;
+
+ private readonly ScrollingPath scrollingPath;
+
+ private readonly NestedOutlineContainer nestedOutlineContainer;
+
+ private readonly Cached pathCache = new Cached();
+
+ public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
+ : base(hitObject)
+ {
+ InternalChildren = new Drawable[]
+ {
+ scrollingPath = new ScrollingPath(),
+ nestedOutlineContainer = new NestedOutlineContainer()
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ HitObject.DefaultsApplied += onDefaultsApplied;
+ computeObjectBounds();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!IsSelected) return;
+
+ nestedOutlineContainer.Position = scrollingPath.Position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
+
+ if (pathCache.IsValid) return;
+
+ scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
+ nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
+
+ pathCache.Validate();
+ }
+
+ private void onDefaultsApplied(HitObject _)
+ {
+ computeObjectBounds();
+ pathCache.Invalidate();
+ }
+
+ private void computeObjectBounds()
+ {
+ minNestedX = HitObject.NestedHitObjects.OfType().Min(nested => nested.OriginalX) - HitObject.OriginalX;
+ maxNestedX = HitObject.NestedHitObjects.OfType().Max(nested => nested.OriginalX) - HitObject.OriginalX;
+ }
+
+ private RectangleF getBoundingBox()
+ {
+ float left = HitObject.OriginalX + minNestedX;
+ float right = HitObject.OriginalX + maxNestedX;
+ float top = HitObjectContainer.PositionAtTime(HitObject.EndTime);
+ float bottom = HitObjectContainer.PositionAtTime(HitObject.StartTime);
+ float objectRadius = CatchHitObject.OBJECT_RADIUS * HitObject.Scale;
+ return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ HitObject.DefaultsApplied -= onDefaultsApplied;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
new file mode 100644
index 0000000000..7f2782a474
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
@@ -0,0 +1,38 @@
+// 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.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Screens.Edit.Compose.Components;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchBlueprintContainer : ComposeBlueprintContainer
+ {
+ public CatchBlueprintContainer(CatchHitObjectComposer composer)
+ : base(composer)
+ {
+ }
+
+ protected override SelectionHandler CreateSelectionHandler() => new CatchSelectionHandler();
+
+ public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case Fruit fruit:
+ return new FruitSelectionBlueprint(fruit);
+
+ case JuiceStream juiceStream:
+ return new JuiceStreamSelectionBlueprint(juiceStream);
+
+ case BananaShower bananaShower:
+ return new BananaShowerSelectionBlueprint(bananaShower);
+ }
+
+ return base.CreateHitObjectBlueprintFor(hitObject);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs
new file mode 100644
index 0000000000..d383eb9ba6
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.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.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.UI;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchEditorPlayfield : CatchPlayfield
+ {
+ // TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen.
+ public CatchEditorPlayfield(BeatmapDifficulty difficulty)
+ : base(difficulty)
+ {
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ // TODO: honor "hit animation" setting?
+ CatcherArea.MovableCatcher.CatchFruitOnPlate = false;
+
+ // TODO: disable hit lighting as well
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
new file mode 100644
index 0000000000..d360274aa6
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
@@ -0,0 +1,54 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchHitObjectComposer : HitObjectComposer
+ {
+ public CatchHitObjectComposer(CatchRuleset ruleset)
+ : base(ruleset)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ LayerBelowRuleset.Add(new PlayfieldBorder
+ {
+ RelativeSizeAxes = Axes.Both,
+ PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
+ });
+ }
+
+ protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) =>
+ new DrawableCatchEditorRuleset(ruleset, beatmap, mods);
+
+ protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[]
+ {
+ new FruitCompositionTool(),
+ new BananaShowerCompositionTool()
+ };
+
+ public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
+ {
+ var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
+ // TODO: implement position snap
+ result.ScreenSpacePosition.X = screenSpacePosition.X;
+ return result;
+ }
+
+ protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this);
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs
new file mode 100644
index 0000000000..beffdf0362
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.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.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ ///
+ /// Utility functions used by the editor.
+ ///
+ public static class CatchHitObjectUtils
+ {
+ ///
+ /// Get the position of the hit object in the playfield based on and .
+ ///
+ public static Vector2 GetStartPosition(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
+ {
+ return new Vector2(hitObject.OriginalX, hitObjectContainer.PositionAtTime(hitObject.StartTime));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
new file mode 100644
index 0000000000..7eebf04ca2
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
@@ -0,0 +1,116 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchSelectionHandler : EditorSelectionHandler
+ {
+ protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
+
+ [Resolved]
+ private Playfield playfield { get; set; }
+
+ public override bool HandleMovement(MoveSelectionEvent moveEvent)
+ {
+ var blueprint = moveEvent.Blueprint;
+ Vector2 originalPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint);
+ Vector2 targetPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta);
+
+ float deltaX = targetPosition.X - originalPosition.X;
+ deltaX = limitMovement(deltaX, EditorBeatmap.SelectedHitObjects);
+
+ if (deltaX == 0)
+ {
+ // Even if there is no positional change, there may be a time change.
+ return true;
+ }
+
+ EditorBeatmap.PerformOnSelection(h =>
+ {
+ if (!(h is CatchHitObject hitObject)) return;
+
+ hitObject.OriginalX += deltaX;
+
+ // Move the nested hit objects to give an instant result before nested objects are recreated.
+ foreach (var nested in hitObject.NestedHitObjects.OfType())
+ nested.OriginalX += deltaX;
+ });
+
+ return true;
+ }
+
+ ///
+ /// Limit positional movement of the objects by the constraint that moved objects should stay in bounds.
+ ///
+ /// The positional movement.
+ /// The objects to be moved.
+ /// The positional movement with the restriction applied.
+ private float limitMovement(float deltaX, IEnumerable movingObjects)
+ {
+ float minX = float.PositiveInfinity;
+ float maxX = float.NegativeInfinity;
+
+ foreach (float x in movingObjects.SelectMany(getOriginalPositions))
+ {
+ minX = Math.Min(minX, x);
+ maxX = Math.Max(maxX, x);
+ }
+
+ // To make an object with position `x` stay in bounds after `deltaX` movement, `0 <= x + deltaX <= WIDTH` should be satisfied.
+ // Subtracting `x`, we get `-x <= deltaX <= WIDTH - x`.
+ // We only need to apply the inequality to extreme values of `x`.
+ float lowerBound = -minX;
+ float upperBound = CatchPlayfield.WIDTH - maxX;
+ // The inequality may be unsatisfiable if the objects were already out of bounds.
+ // In that case, don't move objects at all.
+ if (lowerBound > upperBound)
+ return 0;
+
+ return Math.Clamp(deltaX, lowerBound, upperBound);
+ }
+
+ ///
+ /// Enumerate X positions that should be contained in-bounds after move offset is applied.
+ ///
+ private IEnumerable getOriginalPositions(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case Fruit fruit:
+ yield return fruit.OriginalX;
+
+ break;
+
+ case JuiceStream juiceStream:
+ foreach (var nested in juiceStream.NestedHitObjects.OfType())
+ {
+ // Even if `OriginalX` is outside the playfield, tiny droplets can be moved inside the playfield after the random offset application.
+ if (!(nested is TinyDroplet))
+ yield return nested.OriginalX;
+ }
+
+ break;
+
+ case BananaShower _:
+ // A banana shower occupies the whole screen width.
+ // If the selection contains a banana shower, the selection cannot be moved horizontally.
+ yield return 0;
+ yield return CatchPlayfield.WIDTH;
+
+ break;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
new file mode 100644
index 0000000000..0344709d45
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.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 System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class DrawableCatchEditorRuleset : DrawableCatchRuleset
+ {
+ public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
+ : base(ruleset, beatmap, mods)
+ {
+ }
+
+ protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs
new file mode 100644
index 0000000000..f776fe39c1
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.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.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class FruitCompositionTool : HitObjectCompositionTool
+ {
+ public FruitCompositionTool()
+ : base(nameof(Fruit))
+ {
+ }
+
+ public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
+
+ public override PlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
index e1eceea606..f1b51e51d0 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
@@ -11,7 +10,7 @@ using osu.Game.Users;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModAutoplay : ModAutoplay
+ public class CatchModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
index 5f1736450a..e59a0a0431 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
@@ -5,39 +5,35 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModDifficultyAdjust : ModDifficultyAdjust
+ public class CatchModDifficultyAdjust : ModDifficultyAdjust, IApplicableToBeatmapProcessor
{
- [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
- public BindableNumber CircleSize { get; } = new BindableFloatWithLimitExtension
+ [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
+ public DifficultyBindable CircleSize { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 1,
MaxValue = 10,
- Default = 5,
- Value = 5,
+ ExtendedMaxValue = 11,
+ ReadCurrentFromDifficulty = diff => diff.CircleSize,
};
- [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
- public BindableNumber ApproachRate { get; } = new BindableFloatWithLimitExtension
+ [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
+ public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 1,
MaxValue = 10,
- Default = 5,
- Value = 5,
+ ExtendedMaxValue = 11,
+ ReadCurrentFromDifficulty = diff => diff.ApproachRate,
};
- protected override void ApplyLimits(bool extended)
- {
- base.ApplyLimits(extended);
-
- CircleSize.MaxValue = extended ? 11 : 10;
- ApproachRate.MaxValue = extended ? 11 : 10;
- }
+ [SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
+ public BindableBool HardRockOffsets { get; } = new BindableBool();
public override string SettingDescription
{
@@ -45,30 +41,30 @@ namespace osu.Game.Rulesets.Catch.Mods
{
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
+ string spicyPatterns = HardRockOffsets.IsDefault ? string.Empty : "Spicy patterns";
return string.Join(", ", new[]
{
circleSize,
base.SettingDescription,
- approachRate
+ approachRate,
+ spicyPatterns,
}.Where(s => !string.IsNullOrEmpty(s)));
}
}
- protected override void TransferSettings(BeatmapDifficulty difficulty)
- {
- base.TransferSettings(difficulty);
-
- TransferSetting(CircleSize, difficulty.CircleSize);
- TransferSetting(ApproachRate, difficulty.ApproachRate);
- }
-
protected override void ApplySettings(BeatmapDifficulty difficulty)
{
base.ApplySettings(difficulty);
- ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
- ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
+ if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value;
+ if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value;
+ }
+
+ public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
+ {
+ var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
+ catchProcessor.HardRockOffsets = HardRockOffsets.Value;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
index ced1900ba9..68b6ce96a3 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
@@ -7,11 +7,14 @@ using osu.Game.Rulesets.Catch.Beatmaps;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModHardRock : ModHardRock, IApplicableToBeatmap
+ public class CatchModHardRock : ModHardRock, IApplicableToBeatmapProcessor
{
public override double ScoreMultiplier => 1.12;
- public override bool Ranked => true;
- public void ApplyToBeatmap(IBeatmap beatmap) => CatchBeatmapProcessor.ApplyPositionOffsets(beatmap, this);
+ public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
+ {
+ var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
+ catchProcessor.HardRockOffsets = true;
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
index bba42dea97..f9e106f097 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
@@ -28,10 +28,11 @@ namespace osu.Game.Rulesets.Catch.Mods
catchPlayfield.CatcherArea.MovableCatcher.CatchFruitOnPlate = false;
}
+ protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
+ => ApplyNormalVisibilityState(hitObject, state);
+
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
- base.ApplyNormalVisibilityState(hitObject, state);
-
if (!(hitObject is DrawableCatchHitObject catchDrawable))
return;
@@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Catch.Mods
var offset = hitObject.TimePreempt * fade_out_offset_multiplier;
var duration = offset - hitObject.TimePreempt * fade_out_duration_multiplier;
- using (drawable.BeginAbsoluteSequence(hitObject.StartTime - offset, true))
+ using (drawable.BeginAbsoluteSequence(hitObject.StartTime - offset))
drawable.FadeOut(duration);
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
index 1e42c6a240..73b60f51a4 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
@@ -33,13 +33,13 @@ namespace osu.Game.Rulesets.Catch.Mods
private class MouseInputHelper : Drawable, IKeyBindingHandler, IRequireHighFrequencyMousePosition
{
- private readonly Catcher catcher;
+ private readonly CatcherArea catcherArea;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
public MouseInputHelper(CatchPlayfield playfield)
{
- catcher = playfield.CatcherArea.MovableCatcher;
+ catcherArea = playfield.CatcherArea;
RelativeSizeAxes = Axes.Both;
}
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Mods
protected override bool OnMouseMove(MouseMoveEvent e)
{
- catcher.UpdatePosition(e.MousePosition.X / DrawSize.X * CatchPlayfield.WIDTH);
+ catcherArea.SetCatcherPosition(e.MousePosition.X / DrawSize.X * CatchPlayfield.WIDTH);
return base.OnMouseMove(e);
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index ae45182960..f979e3e0ca 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.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 Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -8,10 +9,11 @@ using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
+using osuTK;
namespace osu.Game.Rulesets.Catch.Objects
{
- public abstract class CatchHitObject : HitObject, IHasXPosition, IHasComboInformation
+ public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation
{
public const float OBJECT_RADIUS = 64;
@@ -20,13 +22,16 @@ namespace osu.Game.Rulesets.Catch.Objects
///
/// The horizontal position of the hit object between 0 and .
///
+ ///
+ /// Only setter is exposed.
+ /// Use or to get the horizontal position.
+ ///
+ [JsonIgnore]
public float X
{
set => OriginalXBindable.Value = value;
}
- float IHasXPosition.X => OriginalXBindable.Value;
-
public readonly Bindable XOffsetBindable = new Bindable();
///
@@ -34,6 +39,7 @@ namespace osu.Game.Rulesets.Catch.Objects
///
public float XOffset
{
+ get => XOffsetBindable.Value;
set => XOffsetBindable.Value = value;
}
@@ -44,7 +50,11 @@ namespace osu.Game.Rulesets.Catch.Objects
/// This value is the original value specified in the beatmap, not affected by the beatmap processing.
/// Use for a gameplay.
///
- public float OriginalX => OriginalXBindable.Value;
+ public float OriginalX
+ {
+ get => OriginalXBindable.Value;
+ set => OriginalXBindable.Value = value;
+ }
///
/// The effective horizontal position of the hit object between 0 and .
@@ -53,9 +63,9 @@ namespace osu.Game.Rulesets.Catch.Objects
/// This value is the original value plus the offset applied by the beatmap processing.
/// Use if a value not affected by the offset is desired.
///
- public float EffectiveX => OriginalXBindable.Value + XOffsetBindable.Value;
+ public float EffectiveX => OriginalX + XOffset;
- public double TimePreempt = 1000;
+ public double TimePreempt { get; set; } = 1000;
public readonly Bindable IndexInBeatmapBindable = new Bindable();
@@ -120,5 +130,24 @@ namespace osu.Game.Rulesets.Catch.Objects
}
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
+
+ #region Hit object conversion
+
+ // The half of the height of the osu! playfield.
+ public const float DEFAULT_LEGACY_CONVERT_Y = 192;
+
+ ///
+ /// The Y position of the hit object is not used in the normal osu!catch gameplay.
+ /// It is preserved to maximize the backward compatibility with the legacy editor, in which the mappers use the Y position to organize the patterns.
+ ///
+ public float LegacyConvertedY { get; set; } = DEFAULT_LEGACY_CONVERT_Y;
+
+ float IHasXPosition.X => OriginalX;
+
+ float IHasYPosition.Y => LegacyConvertedY;
+
+ Vector2 IHasPosition.Position => new Vector2(OriginalX, LegacyConvertedY);
+
+ #endregion
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs
index 140b411c88..7c88090a20 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Bindables;
using osu.Game.Rulesets.Catch.Skinning.Default;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
@@ -9,21 +8,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
///
/// Represents a caught by the catcher.
///
- public class CaughtFruit : CaughtObject, IHasFruitState
+ public class CaughtFruit : CaughtObject
{
- public Bindable VisualRepresentation { get; } = new Bindable();
-
public CaughtFruit()
: base(CatchSkinComponents.Fruit, _ => new FruitPiece())
{
}
-
- public override void CopyStateFrom(IHasCatchObjectState objectState)
- {
- base.CopyStateFrom(objectState);
-
- var fruitState = (IHasFruitState)objectState;
- VisualRepresentation.Value = fruitState.VisualRepresentation.Value;
- }
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs
index 524505d588..d8bce9bb6d 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs
@@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public PalpableCatchHitObject HitObject { get; private set; }
public Bindable AccentColour { get; } = new Bindable();
public Bindable HyperDash { get; } = new Bindable();
+ public Bindable IndexInBeatmap { get; } = new Bindable();
public Vector2 DisplaySize => Size * Scale;
@@ -51,6 +52,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
Rotation = objectState.DisplayRotation;
AccentColour.Value = objectState.AccentColour.Value;
HyperDash.Value = objectState.HyperDash.Value;
+ IndexInBeatmap.Value = objectState.IndexInBeatmap.Value;
}
protected override void FreeAfterUse()
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
index 0b89c46480..0af7ee6c30 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
@@ -3,17 +3,14 @@
using JetBrains.Annotations;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
- public class DrawableFruit : DrawablePalpableCatchHitObject, IHasFruitState
+ public class DrawableFruit : DrawablePalpableCatchHitObject
{
- public Bindable VisualRepresentation { get; } = new Bindable();
-
public DrawableFruit()
: this(null)
{
@@ -27,11 +24,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
- IndexInBeatmap.BindValueChanged(change =>
- {
- VisualRepresentation.Value = (FruitVisualRepresentation)(change.NewValue % 4);
- }, true);
-
ScalingContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(CatchSkinComponents.Fruit),
_ => new FruitPiece());
@@ -44,12 +36,4 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
ScalingContainer.RotateTo((RandomSingle(1) - 0.5f) * 40);
}
}
-
- public enum FruitVisualRepresentation
- {
- Pear,
- Grape,
- Pineapple,
- Raspberry,
- }
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs
index 81b61f0959..be0ee2821e 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs
@@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
Bindable HyperDash { get; }
+ Bindable IndexInBeatmap { get; }
+
Vector2 DisplaySize { get; }
float DisplayRotation { get; }
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasFruitState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasFruitState.cs
deleted file mode 100644
index 2d4de543c3..0000000000
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasFruitState.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.Framework.Bindables;
-
-namespace osu.Game.Rulesets.Catch.Objects.Drawables
-{
- ///
- /// Provides a visual state of a .
- ///
- public interface IHasFruitState : IHasCatchObjectState
- {
- Bindable VisualRepresentation { get; }
- }
-}
diff --git a/osu.Game.Rulesets.Catch/Objects/Fruit.cs b/osu.Game.Rulesets.Catch/Objects/Fruit.cs
index 43486796ad..4818fe2cad 100644
--- a/osu.Game.Rulesets.Catch/Objects/Fruit.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Fruit.cs
@@ -9,5 +9,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public class Fruit : PalpableCatchHitObject
{
public override Judgement CreateJudgement() => new CatchJudgement();
+
+ public static FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => (FruitVisualRepresentation)(indexInBeatmap % 4);
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/FruitVisualRepresentation.cs b/osu.Game.Rulesets.Catch/Objects/FruitVisualRepresentation.cs
new file mode 100644
index 0000000000..7ec7050245
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/FruitVisualRepresentation.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.Catch.Objects
+{
+ public enum FruitVisualRepresentation
+ {
+ Pear,
+ Grape,
+ Pineapple,
+ Raspberry,
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 35fd58826e..3088d024d1 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
+using Newtonsoft.Json;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -25,7 +26,10 @@ namespace osu.Game.Rulesets.Catch.Objects
public int RepeatCount { get; set; }
+ [JsonIgnore]
public double Velocity { get; private set; }
+
+ [JsonIgnore]
public double TickDistance { get; private set; }
///
@@ -113,6 +117,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public float EndX => OriginalX + this.CurvePositionAt(1).X;
+ [JsonIgnore]
public double Duration
{
get => this.SpanCount() * Path.Distance / Velocity;
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs
new file mode 100644
index 0000000000..f1cdb39e91
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs
@@ -0,0 +1,340 @@
+// 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.Utils;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+#nullable enable
+
+namespace osu.Game.Rulesets.Catch.Objects
+{
+ ///
+ /// Represents the path of a juice stream.
+ ///
+ /// A holds a legacy as the representation of the path.
+ /// However, the representation is difficult to work with.
+ /// This represents the path in a more convenient way, a polyline connecting list of s.
+ ///
+ ///
+ /// The path can be regarded as a function from the closed interval [Vertices[0].Distance, Vertices[^1].Distance] to the x position, given by .
+ /// To ensure the path is convertible to a , the slope of the function must not be more than 1 everywhere,
+ /// and this slope condition is always maintained as an invariant.
+ ///
+ ///
+ public class JuiceStreamPath
+ {
+ ///
+ /// The height of legacy osu!standard playfield.
+ /// The sliders converted by are vertically contained in this height.
+ ///
+ internal const float OSU_PLAYFIELD_HEIGHT = 384;
+
+ ///
+ /// The list of vertices of the path, which is represented as a polyline connecting the vertices.
+ ///
+ public IReadOnlyList Vertices => vertices;
+
+ ///
+ /// The current version number.
+ /// This starts from 1 and incremented whenever this is modified.
+ ///
+ public int InvalidationID { get; private set; } = 1;
+
+ ///
+ /// The difference between first vertex's and last vertex's .
+ ///
+ public double Distance => vertices[^1].Distance - vertices[0].Distance;
+
+ ///
+ /// This list should always be non-empty.
+ ///
+ private readonly List vertices = new List
+ {
+ new JuiceStreamPathVertex()
+ };
+
+ ///
+ /// Compute the x-position of the path at the given .
+ ///
+ ///
+ /// When the given distance is outside of the path, the x position at the corresponding endpoint is returned,
+ ///
+ public float PositionAtDistance(double distance)
+ {
+ int index = vertexIndexAtDistance(distance);
+ return positionAtDistance(distance, index);
+ }
+
+ ///
+ /// Remove all vertices of this path, then add a new vertex (0, 0).
+ ///
+ public void Clear()
+ {
+ vertices.Clear();
+ vertices.Add(new JuiceStreamPathVertex());
+ invalidate();
+ }
+
+ ///
+ /// Insert a vertex at given .
+ /// The is used as the position of the new vertex.
+ /// Thus, the set of points of the path is not changed (up to floating-point precision).
+ ///
+ /// The index of the new vertex.
+ public int InsertVertex(double distance)
+ {
+ if (!double.IsFinite(distance))
+ throw new ArgumentOutOfRangeException(nameof(distance));
+
+ int index = vertexIndexAtDistance(distance);
+ float x = positionAtDistance(distance, index);
+ vertices.Insert(index, new JuiceStreamPathVertex(distance, x));
+
+ invalidate();
+ return index;
+ }
+
+ ///
+ /// Move the vertex of given to the given position .
+ /// When the distances between vertices are too small for the new vertex positions, the adjacent vertices are moved towards .
+ ///
+ public void SetVertexPosition(int index, float newX)
+ {
+ if (index < 0 || index >= vertices.Count)
+ throw new ArgumentOutOfRangeException(nameof(index));
+
+ if (!float.IsFinite(newX))
+ throw new ArgumentOutOfRangeException(nameof(newX));
+
+ var newVertex = new JuiceStreamPathVertex(vertices[index].Distance, newX);
+
+ for (int i = index - 1; i >= 0 && !canConnect(vertices[i], newVertex); i--)
+ {
+ float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
+ vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
+ }
+
+ for (int i = index + 1; i < vertices.Count; i++)
+ {
+ float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
+ vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
+ }
+
+ vertices[index] = newVertex;
+
+ invalidate();
+ }
+
+ ///
+ /// Add a new vertex at given and position.
+ /// Adjacent vertices are moved when necessary in the same way as .
+ ///
+ public void Add(double distance, float x)
+ {
+ int index = InsertVertex(distance);
+ SetVertexPosition(index, x);
+ }
+
+ ///
+ /// Remove all vertices that satisfy the given .
+ ///
+ ///
+ /// If all vertices are removed, a new vertex (0, 0) is added.
+ ///
+ /// The predicate to determine whether a vertex should be removed given the vertex and its index in the path.
+ /// The number of removed vertices.
+ public int RemoveVertices(Func predicate)
+ {
+ int index = 0;
+ int removeCount = vertices.RemoveAll(vertex => predicate(vertex, index++));
+
+ if (vertices.Count == 0)
+ vertices.Add(new JuiceStreamPathVertex());
+
+ if (removeCount != 0)
+ invalidate();
+
+ return removeCount;
+ }
+
+ ///
+ /// Recreate this path by using difference set of vertices at given distances.
+ /// In addition to the given , the first vertex and the last vertex are always added to the new path.
+ /// New vertices use the positions on the original path. Thus, s at are preserved.
+ ///
+ public void ResampleVertices(IEnumerable sampleDistances)
+ {
+ var sampledVertices = new List();
+
+ foreach (double distance in sampleDistances)
+ {
+ if (!double.IsFinite(distance))
+ throw new ArgumentOutOfRangeException(nameof(sampleDistances));
+
+ double clampedDistance = Math.Clamp(distance, vertices[0].Distance, vertices[^1].Distance);
+ float x = PositionAtDistance(clampedDistance);
+ sampledVertices.Add(new JuiceStreamPathVertex(clampedDistance, x));
+ }
+
+ sampledVertices.Sort();
+
+ // The first vertex and the last vertex are always used in the result.
+ vertices.RemoveRange(1, vertices.Count - (vertices.Count == 1 ? 1 : 2));
+ vertices.InsertRange(1, sampledVertices);
+
+ invalidate();
+ }
+
+ ///
+ /// Convert a to list of vertices and write the result to this .
+ ///
+ ///
+ /// Duplicated vertices are automatically removed.
+ ///
+ public void ConvertFromSliderPath(SliderPath sliderPath)
+ {
+ var sliderPathVertices = new List();
+ sliderPath.GetPathToProgress(sliderPathVertices, 0, 1);
+
+ double distance = 0;
+
+ vertices.Clear();
+ vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X));
+
+ for (int i = 1; i < sliderPathVertices.Count; i++)
+ {
+ distance += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]);
+
+ if (!Precision.AlmostEquals(vertices[^1].Distance, distance))
+ vertices.Add(new JuiceStreamPathVertex(distance, sliderPathVertices[i].X));
+ }
+
+ invalidate();
+ }
+
+ ///
+ /// Convert the path of this to a and write the result to .
+ /// The resulting slider is "folded" to make it vertically contained in the playfield `(0..)` assuming the slider start position is .
+ ///
+ public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY)
+ {
+ const float margin = 1;
+
+ // Note: these two variables and `sliderPath` are modified by the local functions.
+ double currentDistance = 0;
+ Vector2 lastPosition = new Vector2(vertices[0].X, 0);
+
+ sliderPath.ControlPoints.Clear();
+ sliderPath.ControlPoints.Add(new PathControlPoint(lastPosition));
+
+ for (int i = 1; i < vertices.Count; i++)
+ {
+ sliderPath.ControlPoints[^1].Type.Value = PathType.Linear;
+
+ float deltaX = vertices[i].X - lastPosition.X;
+ double length = vertices[i].Distance - currentDistance;
+
+ // Should satisfy `deltaX^2 + deltaY^2 = length^2`.
+ // By invariants, the expression inside the `sqrt` is (almost) non-negative.
+ double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX));
+
+ // When `deltaY` is small, one segment is always enough.
+ // This case is handled separately to prevent divide-by-zero.
+ if (deltaY <= OSU_PLAYFIELD_HEIGHT / 2 - margin)
+ {
+ float nextX = vertices[i].X;
+ float nextY = (float)(lastPosition.Y + getYDirection() * deltaY);
+ addControlPoint(nextX, nextY);
+ continue;
+ }
+
+ // When `deltaY` is large or when the slider velocity is fast, the segment must be partitioned to subsegments to stay in bounds.
+ for (double currentProgress = 0; currentProgress < deltaY;)
+ {
+ double nextProgress = Math.Min(currentProgress + getMaxDeltaY(), deltaY);
+ float nextX = (float)(vertices[i - 1].X + nextProgress / deltaY * deltaX);
+ float nextY = (float)(lastPosition.Y + getYDirection() * (nextProgress - currentProgress));
+ addControlPoint(nextX, nextY);
+ currentProgress = nextProgress;
+ }
+ }
+
+ int getYDirection()
+ {
+ float lastSliderY = sliderStartY + lastPosition.Y;
+ return lastSliderY < OSU_PLAYFIELD_HEIGHT / 2 ? 1 : -1;
+ }
+
+ float getMaxDeltaY()
+ {
+ float lastSliderY = sliderStartY + lastPosition.Y;
+ return Math.Max(lastSliderY, OSU_PLAYFIELD_HEIGHT - lastSliderY) - margin;
+ }
+
+ void addControlPoint(float nextX, float nextY)
+ {
+ Vector2 nextPosition = new Vector2(nextX, nextY);
+ sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition));
+ currentDistance += Vector2.Distance(lastPosition, nextPosition);
+ lastPosition = nextPosition;
+ }
+ }
+
+ ///
+ /// Find the index at which a new vertex with can be inserted.
+ ///
+ private int vertexIndexAtDistance(double distance)
+ {
+ // The position of `(distance, Infinity)` is uniquely determined because infinite positions are not allowed.
+ int i = vertices.BinarySearch(new JuiceStreamPathVertex(distance, float.PositiveInfinity));
+ return i < 0 ? ~i : i;
+ }
+
+ ///
+ /// Compute the position at the given , assuming is the vertex index returned by .
+ ///
+ private float positionAtDistance(double distance, int index)
+ {
+ if (index <= 0)
+ return vertices[0].X;
+ if (index >= vertices.Count)
+ return vertices[^1].X;
+
+ double length = vertices[index].Distance - vertices[index - 1].Distance;
+ if (Precision.AlmostEquals(length, 0))
+ return vertices[index].X;
+
+ float deltaX = vertices[index].X - vertices[index - 1].X;
+
+ return (float)(vertices[index - 1].X + deltaX * ((distance - vertices[index - 1].Distance) / length));
+ }
+
+ ///
+ /// Check the two vertices can connected directly while satisfying the slope condition.
+ ///
+ private bool canConnect(JuiceStreamPathVertex vertex1, JuiceStreamPathVertex vertex2, float allowance = 0)
+ {
+ double xDistance = Math.Abs((double)vertex2.X - vertex1.X);
+ float length = (float)Math.Abs(vertex2.Distance - vertex1.Distance);
+ return xDistance <= length + allowance;
+ }
+
+ ///
+ /// Move the position of towards the position of
+ /// until the vertex pair satisfies the condition .
+ ///
+ /// The resulting position of .
+ private float clampToConnectablePosition(JuiceStreamPathVertex fixedVertex, JuiceStreamPathVertex movableVertex)
+ {
+ float length = (float)Math.Abs(movableVertex.Distance - fixedVertex.Distance);
+ return Math.Clamp(movableVertex.X, fixedVertex.X - length, fixedVertex.X + length);
+ }
+
+ private void invalidate() => InvalidationID++;
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.cs
new file mode 100644
index 0000000000..58c50603c4
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPathVertex.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 System;
+
+#nullable enable
+
+namespace osu.Game.Rulesets.Catch.Objects
+{
+ ///
+ /// A vertex of a .
+ ///
+ public readonly struct JuiceStreamPathVertex : IComparable
+ {
+ public readonly double Distance;
+
+ public readonly float X;
+
+ public JuiceStreamPathVertex(double distance, float x)
+ {
+ Distance = distance;
+ X = x;
+ }
+
+ public int CompareTo(JuiceStreamPathVertex other)
+ {
+ int c = Distance.CompareTo(other.Distance);
+ return c != 0 ? c : X.CompareTo(other.X);
+ }
+
+ public override string ToString() => $"({Distance}, {X})";
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
index 24c12343e5..4001a4ea76 100644
--- a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.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 Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
@@ -33,6 +34,7 @@ namespace osu.Game.Rulesets.Catch.Objects
///
/// The target fruit if we are to initiate a hyperdash.
///
+ [JsonIgnore]
public CatchHitObject HyperDashTarget
{
get => hyperDashTarget;
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
index 10230b6b78..a81703119a 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
@@ -5,7 +5,6 @@ using System;
using System.Linq;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
-using osu.Game.Replays;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
@@ -13,26 +12,19 @@ using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Catch.Replays
{
- internal class CatchAutoGenerator : AutoGenerator
+ internal class CatchAutoGenerator : AutoGenerator
{
- public const double RELEASE_DELAY = 20;
-
public new CatchBeatmap Beatmap => (CatchBeatmap)base.Beatmap;
public CatchAutoGenerator(IBeatmap beatmap)
: base(beatmap)
{
- Replay = new Replay();
}
- protected Replay Replay;
-
- private CatchReplayFrame currentFrame;
-
- public override Replay Generate()
+ protected override void GenerateFrames()
{
if (Beatmap.HitObjects.Count == 0)
- return Replay;
+ return;
// todo: add support for HT DT
const double dash_speed = Catcher.BASE_SPEED;
@@ -119,15 +111,11 @@ namespace osu.Game.Rulesets.Catch.Replays
}
}
}
-
- return Replay;
}
private void addFrame(double time, float? position = null, bool dashing = false)
{
- var last = currentFrame;
- currentFrame = new CatchReplayFrame(time, position, dashing, last);
- Replay.Frames.Add(currentFrame);
+ Frames.Add(new CatchReplayFrame(time, position, dashing, LastFrame));
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs
deleted file mode 100644
index 0a444d923e..0000000000
--- a/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Rulesets.Scoring;
-
-namespace osu.Game.Rulesets.Catch.Scoring
-{
- public class CatchHitWindows : HitWindows
- {
- public override bool IsHitResultAllowed(HitResult result)
- {
- switch (result)
- {
- case HitResult.Great:
- case HitResult.Miss:
- return true;
- }
-
- return false;
- }
- }
-}
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs
new file mode 100644
index 0000000000..ea8d742b1a
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.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.Catch.Skinning
+{
+ public enum CatchSkinConfiguration
+ {
+ ///
+ /// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
+ ///
+ FlipCatcherPlate
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs
index 51c06c8e37..2db3bae034 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs
@@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
{
public readonly Bindable AccentColour = new Bindable();
public readonly Bindable HyperDash = new Bindable();
+ public readonly Bindable IndexInBeatmap = new Bindable();
[Resolved]
protected IHasCatchObjectState ObjectState { get; private set; }
@@ -37,6 +38,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
AccentColour.BindTo(ObjectState.AccentColour);
HyperDash.BindTo(ObjectState.HyperDash);
+ IndexInBeatmap.BindTo(ObjectState.IndexInBeatmap);
HyperDash.BindValueChanged(hyper =>
{
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs b/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs
new file mode 100644
index 0000000000..e423f21b98
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.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 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.Catch.UI;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Default
+{
+ public class DefaultCatcher : CompositeDrawable
+ {
+ public Bindable CurrentState { get; } = new Bindable();
+
+ private readonly Sprite sprite;
+
+ private readonly Dictionary textures = new Dictionary();
+
+ public DefaultCatcher()
+ {
+ RelativeSizeAxes = Axes.Both;
+ InternalChild = sprite = new Sprite
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore store, Bindable currentState)
+ {
+ CurrentState.BindTo(currentState);
+
+ textures[CatcherAnimationState.Idle] = store.Get(@"Gameplay/catch/fruit-catcher-idle");
+ textures[CatcherAnimationState.Fail] = store.Get(@"Gameplay/catch/fruit-catcher-fail");
+ textures[CatcherAnimationState.Kiai] = store.Get(@"Gameplay/catch/fruit-catcher-kiai");
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ CurrentState.BindValueChanged(state => sprite.Texture = textures[state.NewValue], true);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs
index 49f128c960..cfe0df0c97 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs
@@ -3,7 +3,7 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.Objects;
namespace osu.Game.Rulesets.Catch.Skinning.Default
{
@@ -39,8 +39,10 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
{
base.LoadComplete();
- var fruitState = (IHasFruitState)ObjectState;
- VisualRepresentation.BindTo(fruitState.VisualRepresentation);
+ IndexInBeatmap.BindValueChanged(index =>
+ {
+ VisualRepresentation.Value = Fruit.GetVisualRepresentation(index.NewValue);
+ }, true);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPulpFormation.cs b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPulpFormation.cs
index 88e0b5133a..f097361d2a 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPulpFormation.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPulpFormation.cs
@@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
-using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.Objects;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Default
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
index 1b48832ed6..5e744ec001 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
@@ -1,8 +1,10 @@
// 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.Graphics;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK.Graphics;
@@ -15,66 +17,79 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
///
private bool providesComboCounter => this.HasFont(LegacyFont.Combo);
- public CatchLegacySkinTransformer(ISkinSource source)
- : base(source)
+ public CatchLegacySkinTransformer(ISkin skin)
+ : base(skin)
{
}
public override Drawable GetDrawableComponent(ISkinComponent component)
{
- if (component is HUDSkinComponent hudComponent)
+ if (component is SkinnableTargetComponent targetComponent)
{
- switch (hudComponent.Component)
+ switch (targetComponent.Target)
{
- case HUDSkinComponents.ComboCounter:
- // catch may provide its own combo counter; hide the default.
- return providesComboCounter ? Drawable.Empty() : null;
+ case SkinnableTarget.MainHUDComponents:
+ var components = base.GetDrawableComponent(component) as SkinnableTargetComponentsContainer;
+
+ if (providesComboCounter && components != null)
+ {
+ // catch may provide its own combo counter; hide the default.
+ // todo: this should be done in an elegant way per ruleset, defining which HUD skin components should be displayed.
+ foreach (var legacyComboCounter in components.OfType())
+ legacyComboCounter.HiddenByRulesetImplementation = false;
+ }
+
+ return components;
}
}
- if (!(component is CatchSkinComponent catchSkinComponent))
- return null;
-
- switch (catchSkinComponent.Component)
+ if (component is CatchSkinComponent catchSkinComponent)
{
- case CatchSkinComponents.Fruit:
- if (GetTexture("fruit-pear") != null)
- return new LegacyFruitPiece();
+ switch (catchSkinComponent.Component)
+ {
+ case CatchSkinComponents.Fruit:
+ if (GetTexture("fruit-pear") != null)
+ return new LegacyFruitPiece();
- break;
+ return null;
- case CatchSkinComponents.Banana:
- if (GetTexture("fruit-bananas") != null)
- return new LegacyBananaPiece();
+ case CatchSkinComponents.Banana:
+ if (GetTexture("fruit-bananas") != null)
+ return new LegacyBananaPiece();
- break;
+ return null;
- case CatchSkinComponents.Droplet:
- if (GetTexture("fruit-drop") != null)
- return new LegacyDropletPiece();
+ case CatchSkinComponents.Droplet:
+ if (GetTexture("fruit-drop") != null)
+ return new LegacyDropletPiece();
- break;
+ return null;
- case CatchSkinComponents.CatcherIdle:
- return this.GetAnimation("fruit-catcher-idle", true, true, true) ??
- this.GetAnimation("fruit-ryuuta", true, true, true);
+ case CatchSkinComponents.Catcher:
+ var version = GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1;
- case CatchSkinComponents.CatcherFail:
- return this.GetAnimation("fruit-catcher-fail", true, true, true) ??
- this.GetAnimation("fruit-ryuuta", true, true, true);
+ if (version < 2.3m)
+ {
+ if (GetTexture(@"fruit-ryuuta") != null ||
+ GetTexture(@"fruit-ryuuta-0") != null)
+ return new LegacyCatcherOld();
+ }
- case CatchSkinComponents.CatcherKiai:
- return this.GetAnimation("fruit-catcher-kiai", true, true, true) ??
- this.GetAnimation("fruit-ryuuta", true, true, true);
+ if (GetTexture(@"fruit-catcher-idle") != null ||
+ GetTexture(@"fruit-catcher-idle-0") != null)
+ return new LegacyCatcherNew();
- case CatchSkinComponents.CatchComboCounter:
- if (providesComboCounter)
- return new LegacyCatchComboCounter(Source);
+ return null;
- break;
+ case CatchSkinComponents.CatchComboCounter:
+ if (providesComboCounter)
+ return new LegacyCatchComboCounter(Skin);
+
+ return null;
+ }
}
- return null;
+ return base.GetDrawableComponent(component);
}
public override IBindable GetConfig(TLookup lookup)
@@ -82,15 +97,28 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
switch (lookup)
{
case CatchSkinColour colour:
- var result = (Bindable)Source.GetConfig(new SkinCustomColourLookup(colour));
+ var result = (Bindable)base.GetConfig(new SkinCustomColourLookup(colour));
if (result == null)
return null;
result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value);
return (IBindable)result;
+
+ case CatchSkinConfiguration config:
+ switch (config)
+ {
+ case CatchSkinConfiguration.FlipCatcherPlate:
+ // Don't flip catcher plate contents if the catcher is provided by this legacy skin.
+ if (GetDrawableComponent(new CatchSkinComponent(CatchSkinComponents.Catcher)) != null)
+ return (IBindable)new Bindable();
+
+ break;
+ }
+
+ break;
}
- return Source.GetConfig(lookup);
+ return base.GetConfig(lookup);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyBananaPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs
similarity index 91%
rename from osu.Game.Rulesets.Catch/Skinning/LegacyBananaPiece.cs
rename to osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs
index f80e50c8c0..5bd5b0d4bb 100644
--- a/osu.Game.Rulesets.Catch/Skinning/LegacyBananaPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyBananaPiece.cs
@@ -3,7 +3,7 @@
using osu.Framework.Graphics.Textures;
-namespace osu.Game.Rulesets.Catch.Skinning
+namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
public class LegacyBananaPiece : LegacyCatchHitObjectPiece
{
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs
index 28ee7bd813..33c3867f5a 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
InternalChildren = new Drawable[]
{
- explosion = new LegacyRollingCounter(skin, LegacyFont.Combo)
+ explosion = new LegacyRollingCounter(LegacyFont.Combo)
{
Alpha = 0.65f,
Blending = BlendingParameters.Additive,
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
Origin = Anchor.Centre,
Scale = new Vector2(1.5f),
},
- counter = new LegacyRollingCounter(skin, LegacyFont.Combo)
+ counter = new LegacyRollingCounter(LegacyFont.Combo)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchHitObjectPiece.cs
similarity index 94%
rename from osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs
rename to osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchHitObjectPiece.cs
index 4b1f5a4724..f78724615a 100644
--- a/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchHitObjectPiece.cs
@@ -13,12 +13,13 @@ using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Rulesets.Catch.Skinning
+namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
public abstract class LegacyCatchHitObjectPiece : PoolableDrawable
{
public readonly Bindable AccentColour = new Bindable();
public readonly Bindable HyperDash = new Bindable();
+ public readonly Bindable IndexInBeatmap = new Bindable();
private readonly Sprite colouredSprite;
private readonly Sprite overlaySprite;
@@ -64,6 +65,7 @@ namespace osu.Game.Rulesets.Catch.Skinning
AccentColour.BindTo(ObjectState.AccentColour);
HyperDash.BindTo(ObjectState.HyperDash);
+ IndexInBeatmap.BindTo(ObjectState.IndexInBeatmap);
hyperSprite.Colour = Skin.GetConfig(CatchSkinColour.HyperDashFruit)?.Value ??
Skin.GetConfig(CatchSkinColour.HyperDash)?.Value ??
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs
new file mode 100644
index 0000000000..9df87c92ea
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs
@@ -0,0 +1,69 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Animations;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Legacy
+{
+ public class LegacyCatcherNew : CompositeDrawable
+ {
+ [Resolved]
+ private Bindable currentState { get; set; }
+
+ private readonly Dictionary drawables = new Dictionary();
+
+ private Drawable currentDrawable;
+
+ public LegacyCatcherNew()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ foreach (var state in Enum.GetValues(typeof(CatcherAnimationState)).Cast())
+ {
+ AddInternal(drawables[state] = getDrawableFor(state).With(d =>
+ {
+ d.Anchor = Anchor.TopCentre;
+ d.Origin = Anchor.TopCentre;
+ d.RelativeSizeAxes = Axes.Both;
+ d.Size = Vector2.One;
+ d.FillMode = FillMode.Fit;
+ d.Alpha = 0;
+ }));
+ }
+
+ currentDrawable = drawables[CatcherAnimationState.Idle];
+
+ Drawable getDrawableFor(CatcherAnimationState state) =>
+ skin.GetAnimation(@$"fruit-catcher-{state.ToString().ToLowerInvariant()}", true, true, true) ??
+ skin.GetAnimation(@"fruit-catcher-idle", true, true, true);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ currentState.BindValueChanged(state =>
+ {
+ currentDrawable.Alpha = 0;
+ currentDrawable = drawables[state.NewValue];
+ currentDrawable.Alpha = 1;
+
+ (currentDrawable as IFramedAnimation)?.GotoFrame(0);
+ }, true);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs
new file mode 100644
index 0000000000..3e679171b2
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs
@@ -0,0 +1,32 @@
+// 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.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Legacy
+{
+ public class LegacyCatcherOld : CompositeDrawable
+ {
+ public LegacyCatcherOld()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ InternalChild = skin.GetAnimation(@"fruit-ryuuta", true, true, true).With(d =>
+ {
+ d.Anchor = Anchor.TopCentre;
+ d.Origin = Anchor.TopCentre;
+ d.RelativeSizeAxes = Axes.Both;
+ d.Size = Vector2.One;
+ d.FillMode = FillMode.Fit;
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyDropletPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs
similarity index 93%
rename from osu.Game.Rulesets.Catch/Skinning/LegacyDropletPiece.cs
rename to osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs
index 8f4331d2a3..2c5cbe1e41 100644
--- a/osu.Game.Rulesets.Catch/Skinning/LegacyDropletPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyDropletPiece.cs
@@ -4,7 +4,7 @@
using osu.Framework.Graphics.Textures;
using osuTK;
-namespace osu.Game.Rulesets.Catch.Skinning
+namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
public class LegacyDropletPiece : LegacyCatchHitObjectPiece
{
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs
index 969cc38e5b..f002bab219 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs
@@ -1,23 +1,20 @@
// 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.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.Objects;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
internal class LegacyFruitPiece : LegacyCatchHitObjectPiece
{
- public readonly Bindable VisualRepresentation = new Bindable();
-
protected override void LoadComplete()
{
base.LoadComplete();
- var fruitState = (IHasFruitState)ObjectState;
- VisualRepresentation.BindTo(fruitState.VisualRepresentation);
-
- VisualRepresentation.BindValueChanged(visual => setTexture(visual.NewValue), true);
+ IndexInBeatmap.BindValueChanged(index =>
+ {
+ setTexture(Fruit.GetVisualRepresentation(index.NewValue));
+ }, true);
}
private void setTexture(FruitVisualRepresentation visualRepresentation)
diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs
index 75feb21298..ad344ff2dd 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs
@@ -25,9 +25,9 @@ namespace osu.Game.Rulesets.Catch.UI
{
}
- protected override void SkinChanged(ISkinSource skin, bool allowFallback)
+ protected override void SkinChanged(ISkinSource skin)
{
- base.SkinChanged(skin, allowFallback);
+ base.SkinChanged(skin);
ComboCounter?.UpdateCombo(currentCombo);
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index 0e1ef90737..05cd29dff5 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -1,10 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
@@ -28,20 +26,18 @@ namespace osu.Game.Rulesets.Catch.UI
///
public const float CENTER_X = WIDTH / 2;
+ [Cached]
+ private readonly DroppedObjectContainer droppedObjectContainer;
+
internal readonly CatcherArea CatcherArea;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
// only check the X position; handle all vertical space.
base.ReceivePositionalInputAt(new Vector2(screenSpacePos.X, ScreenSpaceDrawQuad.Centre.Y));
- public CatchPlayfield(BeatmapDifficulty difficulty, Func> createDrawableRepresentation)
+ public CatchPlayfield(BeatmapDifficulty difficulty)
{
- var droppedObjectContainer = new Container
- {
- RelativeSizeAxes = Axes.Both,
- };
-
- CatcherArea = new CatcherArea(droppedObjectContainer, difficulty)
+ CatcherArea = new CatcherArea(difficulty)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft,
@@ -49,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.UI
InternalChildren = new[]
{
- droppedObjectContainer,
+ droppedObjectContainer = new DroppedObjectContainer(),
CatcherArea.MovableCatcher.CreateProxiedContent(),
HitObjectContainer.CreateProxy(),
// This ordering (`CatcherArea` before `HitObjectContainer`) is important to
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 0d6a577d1e..57523d3505 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -7,10 +7,8 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
-using osu.Framework.Input.Bindings;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
@@ -25,7 +23,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI
{
- public class Catcher : SkinReloadableDrawable, IKeyBindingHandler
+ public class Catcher : SkinReloadableDrawable
{
///
/// The default colour used to tint hyper-dash fruit, along with the moving catcher, its trail
@@ -54,9 +52,9 @@ namespace osu.Game.Rulesets.Catch.UI
public const double BASE_SPEED = 1.0;
///
- /// The amount by which caught fruit should be offset from the plate surface to make them look visually "caught".
+ /// The current speed of the catcher.
///
- public const float CAUGHT_FRUIT_VERTICAL_OFFSET = -5;
+ public double Speed => (Dashing ? 1 : 0.5) * BASE_SPEED * hyperDashModifier;
///
/// The amount by which caught fruit should be scaled down to fit on the plate.
@@ -76,26 +74,26 @@ namespace osu.Game.Rulesets.Catch.UI
///
/// Contains objects dropped from the plate.
///
- private readonly Container droppedObjectTarget;
+ [Resolved]
+ private DroppedObjectContainer droppedObjectTarget { get; set; }
- public CatcherAnimationState CurrentState { get; private set; }
+ public CatcherAnimationState CurrentState
+ {
+ get => Body.AnimationState.Value;
+ private set => Body.AnimationState.Value = value;
+ }
///
/// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable.
///
public 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
+ set
{
if (value == dashing) return;
@@ -105,38 +103,40 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
+ ///
+ /// The currently facing direction.
+ ///
+ public Direction VisualDirection { get; set; } = Direction.Right;
+
+ ///
+ /// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
+ ///
+ private bool flipCatcherPlate;
+
///
/// Width of the area that can be used to attempt catches during gameplay.
///
private readonly float catchWidth;
- private readonly CatcherSprite catcherIdle;
- private readonly CatcherSprite catcherKiai;
- private readonly CatcherSprite catcherFail;
-
- private CatcherSprite currentCatcher;
+ internal readonly SkinnableCatcher Body;
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
- private int currentDirection;
-
private double hyperDashModifier = 1;
private int hyperDashDirection;
private float hyperDashTargetPosition;
private Bindable hitLighting;
- private readonly DrawablePool hitExplosionPool;
- private readonly Container hitExplosionContainer;
+ private readonly HitExplosionContainer hitExplosionContainer;
private readonly DrawablePool caughtFruitPool;
private readonly DrawablePool caughtBananaPool;
private readonly DrawablePool caughtDropletPool;
- public Catcher([NotNull] Container trailsTarget, [NotNull] Container droppedObjectTarget, BeatmapDifficulty difficulty = null)
+ public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null)
{
this.trailsTarget = trailsTarget;
- this.droppedObjectTarget = droppedObjectTarget;
Origin = Anchor.TopCentre;
@@ -148,7 +148,6 @@ namespace osu.Game.Rulesets.Catch.UI
InternalChildren = new Drawable[]
{
- hitExplosionPool = new DrawablePool(10),
caughtFruitPool = new DrawablePool(50),
caughtBananaPool = new DrawablePool(100),
// less capacity is needed compared to fruit because droplet is not stacked
@@ -157,23 +156,11 @@ namespace osu.Game.Rulesets.Catch.UI
{
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
+ // offset fruit vertically to better place "above" the plate.
+ Y = -5
},
- 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,
- },
- hitExplosionContainer = new Container
+ Body = new SkinnableCatcher(),
+ hitExplosionContainer = new HitExplosionContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
@@ -186,8 +173,6 @@ namespace osu.Game.Rulesets.Catch.UI
{
hitLighting = config.GetBindable(OsuSetting.HitLighting);
trails = new CatcherTrailDisplay(this);
-
- updateCatcher();
}
protected override void LoadComplete()
@@ -275,17 +260,16 @@ namespace osu.Game.Rulesets.Catch.UI
SetHyperDashState();
if (result.IsHit)
- updateState(hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle);
+ CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
else if (!(hitObject is Banana))
- updateState(CatcherAnimationState.Fail);
+ CurrentState = CatcherAnimationState.Fail;
}
public void OnRevertResult(DrawableCatchHitObject drawableObject, JudgementResult result)
{
var catchResult = (CatchJudgementResult)result;
- if (CurrentState != catchResult.CatcherAnimationState)
- updateState(catchResult.CatcherAnimationState);
+ CurrentState = catchResult.CatcherAnimationState;
if (HyperDashing != catchResult.CatcherHyperDash)
{
@@ -297,7 +281,6 @@ namespace osu.Game.Rulesets.Catch.UI
caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject);
droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject);
- hitExplosionContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject);
}
///
@@ -331,55 +314,6 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
- 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;
- }
-
- 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;
- }
- }
-
///
/// Drop any fruit off the plate.
///
@@ -399,9 +333,9 @@ namespace osu.Game.Rulesets.Catch.UI
private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing;
- protected override void SkinChanged(ISkinSource skin, bool allowFallback)
+ protected override void SkinChanged(ISkinSource skin)
{
- base.SkinChanged(skin, allowFallback);
+ base.SkinChanged(skin);
hyperDashColour =
skin.GetConfig(CatchSkinColour.HyperDash)?.Value ??
@@ -414,6 +348,8 @@ namespace osu.Game.Rulesets.Catch.UI
trails.HyperDashTrailsColour = hyperDashColour;
trails.EndGlowSpritesColour = hyperDashEndGlowColour;
+ flipCatcherPlate = skin.GetConfig(CatchSkinConfiguration.FlipCatcherPlate)?.Value ?? true;
+
runHyperDashStateTransition(HyperDashing);
}
@@ -421,14 +357,9 @@ namespace osu.Game.Rulesets.Catch.UI
{
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));
+ var scaleFromDirection = new Vector2((int)VisualDirection, 1);
+ Body.Scale = scaleFromDirection;
+ caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
// Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
@@ -439,38 +370,6 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
- 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 placeCaughtObject(DrawablePalpableCatchHitObject drawableObject, Vector2 position)
{
var caughtObject = getCaughtObject(drawableObject.HitObject);
@@ -496,9 +395,6 @@ namespace osu.Game.Rulesets.Catch.UI
float adjustedRadius = displayRadius * lenience_adjust;
float checkDistance = MathF.Pow(adjustedRadius, 2);
- // offset fruit vertically to better place "above" the plate.
- position.Y += CAUGHT_FRUIT_VERTICAL_OFFSET;
-
while (caughtObjectContainer.Any(f => Vector2Extensions.DistanceSquared(f.Position, position) < checkDistance))
{
position.X += RNG.NextSingle(-adjustedRadius, adjustedRadius);
@@ -508,15 +404,8 @@ namespace osu.Game.Rulesets.Catch.UI
return position;
}
- private void addLighting(CatchHitObject hitObject, float x, Color4 colour)
- {
- HitExplosion hitExplosion = hitExplosionPool.Get();
- hitExplosion.HitObject = hitObject;
- hitExplosion.X = x;
- hitExplosion.Scale = new Vector2(hitObject.Scale);
- hitExplosion.ObjectColour = colour;
- hitExplosionContainer.Add(hitExplosion);
- }
+ private void addLighting(CatchHitObject hitObject, float x, Color4 colour) =>
+ hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, x, hitObject.Scale, colour, hitObject.RandomSeed));
private CaughtObject getCaughtObject(PalpableCatchHitObject source)
{
@@ -580,7 +469,7 @@ namespace osu.Game.Rulesets.Catch.UI
break;
case DroppedObjectAnimation.Explode:
- var originalX = droppedObjectTarget.ToSpaceOfOtherDrawable(d.DrawPosition, caughtObjectContainer).X * Scale.X;
+ float originalX = droppedObjectTarget.ToSpaceOfOtherDrawable(d.DrawPosition, caughtObjectContainer).X * caughtObjectContainer.Scale.X;
d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine);
d.MoveToX(d.X + originalX * 6, 1000);
d.FadeOut(750);
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index 44adbd5512..fea314df8d 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -1,8 +1,10 @@
// 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.Containers;
+using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects.Drawables;
@@ -14,14 +16,21 @@ using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
- public class CatcherArea : Container
+ public class CatcherArea : Container, IKeyBindingHandler
{
public const float CATCHER_SIZE = 106.75f;
public readonly Catcher MovableCatcher;
private readonly CatchComboDisplay comboDisplay;
- public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null)
+ ///
+ /// -1 when only left button is pressed.
+ /// 1 when only right button is pressed.
+ /// 0 when none or both left and right buttons are pressed.
+ ///
+ private int currentDirection;
+
+ public CatcherArea(BeatmapDifficulty difficulty = null)
{
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
Children = new Drawable[]
@@ -35,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.UI
Margin = new MarginPadding { Bottom = 350f },
X = CatchPlayfield.CENTER_X
},
- MovableCatcher = new Catcher(this, droppedObjectContainer, difficulty) { X = CatchPlayfield.CENTER_X },
+ MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X },
};
}
@@ -63,16 +72,73 @@ namespace osu.Game.Rulesets.Catch.UI
MovableCatcher.OnRevertResult(hitObject, result);
}
+ protected override void Update()
+ {
+ base.Update();
+
+ var replayState = (GetContainingInputManager().CurrentState as RulesetInputManagerInputState)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState;
+
+ SetCatcherPosition(
+ replayState?.CatcherX ??
+ (float)(MovableCatcher.X + MovableCatcher.Speed * currentDirection * Clock.ElapsedFrameTime));
+ }
+
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
- var state = (GetContainingInputManager().CurrentState as RulesetInputManagerInputState)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState;
-
- if (state?.CatcherX != null)
- MovableCatcher.X = state.CatcherX.Value;
-
comboDisplay.X = MovableCatcher.X;
}
+
+ public void SetCatcherPosition(float X)
+ {
+ float lastPosition = MovableCatcher.X;
+ float newPosition = Math.Clamp(X, 0, CatchPlayfield.WIDTH);
+
+ MovableCatcher.X = newPosition;
+
+ if (lastPosition < newPosition)
+ MovableCatcher.VisualDirection = Direction.Right;
+ else if (lastPosition > newPosition)
+ MovableCatcher.VisualDirection = Direction.Left;
+ }
+
+ public bool OnPressed(CatchAction action)
+ {
+ switch (action)
+ {
+ case CatchAction.MoveLeft:
+ currentDirection--;
+ return true;
+
+ case CatchAction.MoveRight:
+ currentDirection++;
+ return true;
+
+ case CatchAction.Dash:
+ MovableCatcher.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:
+ MovableCatcher.Dashing = false;
+ break;
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
deleted file mode 100644
index ef69e3d2d1..0000000000
--- a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
-using osu.Game.Skinning;
-using osuTK;
-
-namespace osu.Game.Rulesets.Catch.UI
-{
- public class CatcherSprite : SkinnableDrawable
- {
- 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.5f, 0.06f) * CatcherArea.CATCHER_SIZE;
- }
-
- private static CatchSkinComponents componentFromState(CatcherAnimationState state)
- {
- switch (state)
- {
- 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/CatcherTrail.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs
new file mode 100644
index 0000000000..c961d98dc5
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs
@@ -0,0 +1,44 @@
+// 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.Framework.Graphics.Pooling;
+using osu.Framework.Timing;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ ///
+ /// A trail of the catcher.
+ /// It also represents a hyper dash afterimage.
+ ///
+ public class CatcherTrail : PoolableDrawable
+ {
+ public CatcherAnimationState AnimationState
+ {
+ set => body.AnimationState.Value = value;
+ }
+
+ private readonly SkinnableCatcher body;
+
+ public CatcherTrail()
+ {
+ Size = new Vector2(CatcherArea.CATCHER_SIZE);
+ Origin = Anchor.TopCentre;
+ Blending = BlendingParameters.Additive;
+ InternalChild = body = new SkinnableCatcher
+ {
+ // Using a frozen clock because trails should not be animated when the skin has an animated catcher.
+ // TODO: The animation should be frozen at the animation frame at the time of the trail generation.
+ Clock = new FramedClock(new ManualClock()),
+ };
+ }
+
+ protected override void FreeAfterUse()
+ {
+ ClearTransforms();
+ Alpha = 1;
+ base.FreeAfterUse();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
index fa65190032..b59fabcb70 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
@@ -4,10 +4,8 @@
using System;
using JetBrains.Annotations;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
-using osu.Framework.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
@@ -21,11 +19,11 @@ namespace osu.Game.Rulesets.Catch.UI
{
private readonly Catcher catcher;
- private readonly DrawablePool trailPool;
+ private readonly DrawablePool trailPool;
- private readonly Container dashTrails;
- private readonly Container hyperDashTrails;
- private readonly Container endGlowSprites;
+ private readonly Container dashTrails;
+ private readonly Container hyperDashTrails;
+ private readonly Container endGlowSprites;
private Color4 hyperDashTrailsColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
@@ -85,10 +83,10 @@ namespace osu.Game.Rulesets.Catch.UI
InternalChildren = new Drawable[]
{
- trailPool = new DrawablePool(30),
- 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 },
+ trailPool = new DrawablePool(30),
+ 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 },
};
}
@@ -118,17 +116,12 @@ namespace osu.Game.Rulesets.Catch.UI
Scheduler.AddDelayed(displayTrail, catcher.HyperDashing ? 25 : 50);
}
- private CatcherTrailSprite createTrailSprite(Container target)
+ private CatcherTrail createTrailSprite(Container target)
{
- var texture = (catcher.CurrentDrawableCatcher as TextureAnimation)?.CurrentFrame ?? ((Sprite)catcher.CurrentDrawableCatcher).Texture;
+ CatcherTrail sprite = trailPool.Get();
- CatcherTrailSprite sprite = trailPool.Get();
-
- sprite.Texture = texture;
- sprite.Anchor = catcher.Anchor;
- sprite.Scale = catcher.Scale;
- sprite.Blending = BlendingParameters.Additive;
- sprite.RelativePositionAxes = catcher.RelativePositionAxes;
+ sprite.AnimationState = catcher.CurrentState;
+ sprite.Scale = catcher.Scale * catcher.Body.Scale;
sprite.Position = catcher.Position;
target.Add(sprite);
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs
deleted file mode 100644
index 0e3e409fac..0000000000
--- a/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs
+++ /dev/null
@@ -1,40 +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.Framework.Graphics.Pooling;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
-using osuTK;
-
-namespace osu.Game.Rulesets.Catch.UI
-{
- public class CatcherTrailSprite : PoolableDrawable
- {
- public Texture Texture
- {
- set => sprite.Texture = value;
- }
-
- private readonly Sprite sprite;
-
- public CatcherTrailSprite()
- {
- InternalChild = sprite = new Sprite
- {
- RelativeSizeAxes = Axes.Both
- };
-
- 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;
- }
-
- protected override void FreeAfterUse()
- {
- ClearTransforms();
- base.FreeAfterUse();
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePosition.cs b/osu.Game.Rulesets.Catch/UI/Direction.cs
similarity index 60%
rename from osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePosition.cs
rename to osu.Game.Rulesets.Catch/UI/Direction.cs
index 219dad566d..65f064b7fb 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePosition.cs
+++ b/osu.Game.Rulesets.Catch/UI/Direction.cs
@@ -1,11 +1,11 @@
// 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.Edit.Blueprints
+namespace osu.Game.Rulesets.Catch.UI
{
- public enum HoldNotePosition
+ public enum Direction
{
- Start,
- End
+ Right = 1,
+ Left = -1
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
index 9389fa803b..8b6a074426 100644
--- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.UI
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);
- protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty, CreateDrawableRepresentation);
+ protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer();
diff --git a/osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs b/osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs
new file mode 100644
index 0000000000..b44b0caae4
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs
@@ -0,0 +1,17 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ public class DroppedObjectContainer : Container
+ {
+ public DroppedObjectContainer()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
index 26627422e1..d9ab428231 100644
--- a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
+++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
@@ -5,31 +5,16 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
-using osu.Framework.Graphics.Pooling;
using osu.Framework.Utils;
-using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Objects.Pooling;
+using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI
{
- public class HitExplosion : PoolableDrawable
+ public class HitExplosion : PoolableDrawableWithLifetime
{
- private Color4 objectColour;
- public CatchHitObject HitObject;
-
- public Color4 ObjectColour
- {
- get => objectColour;
- set
- {
- if (objectColour == value) return;
-
- objectColour = value;
- onColourChanged();
- }
- }
-
private readonly CircularContainer largeFaint;
private readonly CircularContainer smallFaint;
private readonly CircularContainer directionalGlow1;
@@ -83,9 +68,19 @@ namespace osu.Game.Rulesets.Catch.UI
};
}
- protected override void PrepareForUse()
+ protected override void OnApply(HitExplosionEntry entry)
{
- base.PrepareForUse();
+ X = entry.Position;
+ Scale = new Vector2(entry.Scale);
+ setColour(entry.ObjectColour);
+
+ using (BeginAbsoluteSequence(entry.LifetimeStart))
+ applyTransforms(entry.RNGSeed);
+ }
+
+ private void applyTransforms(int randomSeed)
+ {
+ ClearTransforms(true);
const double duration = 400;
@@ -96,14 +91,13 @@ namespace osu.Game.Rulesets.Catch.UI
.FadeOut(duration * 2);
const float angle_variangle = 15; // should be less than 45
- directionalGlow1.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle);
- directionalGlow2.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle);
+ directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 4);
+ directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 5);
- this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out);
- Expire(true);
+ this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out).Expire();
}
- private void onColourChanged()
+ private void setColour(Color4 objectColour)
{
const float roundness = 100;
diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs b/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs
new file mode 100644
index 0000000000..094d88243a
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.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.Pooling;
+using osu.Game.Rulesets.Objects.Pooling;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ public class HitExplosionContainer : PooledDrawableWithLifetimeContainer
+ {
+ protected override bool RemoveRewoundEntry => true;
+
+ private readonly DrawablePool pool;
+
+ public HitExplosionContainer()
+ {
+ AddInternal(pool = new DrawablePool(10));
+ }
+
+ protected override HitExplosion GetDrawable(HitExplosionEntry entry) => pool.Get(d => d.Apply(entry));
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.cs b/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.cs
new file mode 100644
index 0000000000..b142962a8a
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.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.Graphics.Performance;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ public class HitExplosionEntry : LifetimeEntry
+ {
+ public readonly float Position;
+ public readonly float Scale;
+ public readonly Color4 ObjectColour;
+ public readonly int RNGSeed;
+
+ public HitExplosionEntry(double startTime, float position, float scale, Color4 objectColour, int rngSeed)
+ {
+ LifetimeStart = startTime;
+ Position = position;
+ Scale = scale;
+ ObjectColour = objectColour;
+ RNGSeed = rngSeed;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs b/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs
new file mode 100644
index 0000000000..fc34ba4c8b
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.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.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Catch.Skinning.Default;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ ///
+ /// The visual representation of the .
+ /// It includes the body part of the catcher and the catcher plate.
+ ///
+ public class SkinnableCatcher : SkinnableDrawable
+ {
+ ///
+ /// This is used by skin elements to determine which texture of the catcher is used.
+ ///
+ [Cached]
+ public readonly Bindable AnimationState = new Bindable();
+
+ public SkinnableCatcher()
+ : base(new CatchSkinComponent(CatchSkinComponents.Catcher), _ => new DefaultCatcher())
+ {
+ Anchor = Anchor.TopCentre;
+ // 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.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs
index 176fbba921..124e1a35f9 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs
@@ -1,31 +1,54 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Timing;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene
{
- [Cached(Type = typeof(IAdjustableClock))]
- private readonly IAdjustableClock clock = new StopwatchClock();
+ protected override Container Content => blueprints ?? base.Content;
- protected ManiaSelectionBlueprintTestScene()
+ private readonly Container blueprints;
+
+ [Cached(typeof(Playfield))]
+ public Playfield Playfield { get; }
+
+ private readonly ScrollingTestContainer scrollingTestContainer;
+
+ protected ScrollingDirection Direction
{
- Add(new Column(0)
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- AccentColour = Color4.OrangeRed,
- Clock = new FramedClock(new StopwatchClock()), // No scroll
- });
+ set => scrollingTestContainer.Direction = value;
}
- public ManiaPlayfield Playfield => null;
+ protected ManiaSelectionBlueprintTestScene(int columns)
+ {
+ var stageDefinitions = new List { new StageDefinition { Columns = columns } };
+ base.Content.Child = scrollingTestContainer = new ScrollingTestContainer(ScrollingDirection.Up)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ Playfield = new ManiaPlayfield(stageDefinitions)
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ blueprints = new Container
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ }
+ };
+
+ AddToggleStep("Downward scroll", b => Direction = b ? ScrollingDirection.Down : ScrollingDirection.Up);
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs
index 24f4c6858e..9953b8e3c0 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs
@@ -1,55 +1,32 @@
// 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.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.UI.Scrolling;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneHoldNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
{
- private readonly DrawableHoldNote drawableObject;
-
- protected override Container Content => content ?? base.Content;
- private readonly Container content;
-
public TestSceneHoldNoteSelectionBlueprint()
+ : base(4)
{
- var holdNote = new HoldNote { Column = 0, Duration = 1000 };
- holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
-
- base.Content.Child = content = new ScrollingTestContainer(ScrollingDirection.Down)
+ for (int i = 0; i < 4; i++)
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- AutoSizeAxes = Axes.Y,
- Width = 50,
- Child = drawableObject = new DrawableHoldNote(holdNote)
+ var holdNote = new HoldNote
{
- Height = 300,
- AccentColour = { Value = OsuColour.Gray(0.3f) }
- }
- };
+ Column = i,
+ StartTime = i * 100,
+ Duration = 500
+ };
+ holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- AddBlueprint(new HoldNoteSelectionBlueprint(drawableObject));
- }
-
- protected override void Update()
- {
- base.Update();
-
- foreach (var nested in drawableObject.NestedHitObjects)
- {
- double finalPosition = (nested.HitObject.StartTime - drawableObject.HitObject.StartTime) / drawableObject.HitObject.Duration;
- nested.Y = (float)(-finalPosition * content.DrawHeight);
+ var drawableHitObject = new DrawableHoldNote(holdNote);
+ Playfield.Add(drawableHitObject);
+ AddBlueprint(new HoldNoteSelectionBlueprint(holdNote), drawableHitObject);
}
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
index aaf96c63a6..01d80881fa 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
@@ -12,7 +12,7 @@ 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.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Skinning.Default;
@@ -184,8 +184,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
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);
+ 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)
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs
index 36c34a8fb9..a162c5ec44 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs
@@ -15,7 +15,6 @@ 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.Editor
@@ -35,7 +34,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test]
public void TestPlaceBeforeCurrentTimeDownwards()
{
- AddStep("move mouse before current time", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single().ScreenSpaceDrawQuad.BottomLeft - new Vector2(0, 10)));
+ AddStep("move mouse before current time", () =>
+ {
+ var column = this.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(-100));
+ });
AddStep("click", () => InputManager.Click(MouseButton.Left));
@@ -45,7 +48,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test]
public void TestPlaceAfterCurrentTimeDownwards()
{
- AddStep("move mouse after current time", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single()));
+ AddStep("move mouse after current time", () =>
+ {
+ var column = this.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(100));
+ });
AddStep("click", () => InputManager.Click(MouseButton.Left));
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs
index 0e47a12a8e..3586eecc44 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs
@@ -1,40 +1,32 @@
// 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.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.UI.Scrolling;
-using osu.Game.Tests.Visual;
-using osuTK;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
{
- protected override Container Content => content ?? base.Content;
- private readonly Container content;
-
public TestSceneNoteSelectionBlueprint()
+ : base(4)
{
- var note = new Note { Column = 0 };
- note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
-
- DrawableNote drawableObject;
-
- base.Content.Child = content = new ScrollingTestContainer(ScrollingDirection.Down)
+ for (int i = 0; i < 4; i++)
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(50, 20),
- Child = drawableObject = new DrawableNote(note)
- };
+ var note = new Note
+ {
+ Column = i,
+ StartTime = i * 200,
+ };
+ note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- AddBlueprint(new NoteSelectionBlueprint(drawableObject));
+ var drawableHitObject = new DrawableNote(note);
+ Playfield.Add(drawableHitObject);
+ AddBlueprint(new NoteSelectionBlueprint(note), drawableHitObject);
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
index 96444fd316..b7d7af6b8c 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[SetUp]
public void SetUp() => Schedule(() =>
{
- SetContents(() => new FillFlowContainer
+ SetContents(_ => new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs
index bde323f187..106b2d188d 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new FillFlowContainer
+ SetContents(_ => new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
- Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 0), _ => new DefaultColumnBackground())
+ Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
{
RelativeSizeAxes = Axes.Both
}
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
- Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 1), _ => new DefaultColumnBackground())
+ Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => 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
index 4392666cb7..215f8fb1d5 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new FillFlowContainer
+ SetContents(_ => new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
index dcb25f21ba..75a5495078 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
if (hitWindows.IsHitResultAllowed(result))
{
- AddStep("Show " + result.GetDescription(), () => SetContents(() =>
+ AddStep("Show " + result.GetDescription(), () => SetContents(_ =>
new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
{
Type = result
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
index 4dc6700786..004793e1e5 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() =>
+ SetContents(_ =>
{
var pool = new DrawablePool(5);
hitExplosionPools.Add(pool);
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs
index 7e80419944..7564bd84ad 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new FillFlowContainer
+ SetContents(_ => new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
- Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, 0), _ => new DefaultKeyArea())
+ Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
{
RelativeSizeAxes = Axes.Both
},
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
- Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, 1), _ => new DefaultKeyArea())
+ Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
{
RelativeSizeAxes = Axes.Both
},
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs
index 161eda650e..c7dc5fc8b5 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
new StageDefinition { Columns = 2 }
};
- SetContents(() => new ManiaPlayfield(stageDefinitions));
+ SetContents(_ => new ManiaPlayfield(stageDefinitions));
});
}
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
new StageDefinition { Columns = 2 }
};
- SetContents(() => new ManiaPlayfield(stageDefinitions));
+ SetContents(_ => new ManiaPlayfield(stageDefinitions));
});
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs
index 37b97a444a..7804261906 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() =>
+ SetContents(_ =>
{
ManiaAction normalAction = ManiaAction.Key1;
ManiaAction specialAction = ManiaAction.Special1;
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
index a15fb392d6..410a43fc73 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: new StageDefinition { Columns = 4 }),
+ SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: new StageDefinition { Columns = 4 }),
_ => new DefaultStageBackground())
{
Anchor = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
index bceee1c599..27e97152bc 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: new StageDefinition { Columns = 4 }), _ => null)
+ SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: new StageDefinition { Columns = 4 }), _ => null)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
new file mode 100644
index 0000000000..4a6c59e297
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
@@ -0,0 +1,67 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+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.UI.Scrolling;
+using osu.Game.Tests.Visual;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestSceneDrawableManiaHitObject : OsuTestScene
+ {
+ private readonly ManualClock clock = new ManualClock();
+
+ private Column column;
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ Child = new ScrollingTestContainer(ScrollingDirection.Down)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.X,
+ RelativeSizeAxes = Axes.Y,
+ TimeRange = 2000,
+ Clock = new FramedClock(clock),
+ Child = column = new Column(0)
+ {
+ Action = { Value = ManiaAction.Key1 },
+ Height = 0.85f,
+ AccentColour = Color4.Gray
+ },
+ };
+ });
+
+ [Test]
+ public void TestHoldNoteHeadVisibility()
+ {
+ DrawableHoldNote note = null;
+ AddStep("Add hold note", () =>
+ {
+ var h = new HoldNote
+ {
+ StartTime = 0,
+ Duration = 1000
+ };
+ h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ column.Add(note = new DrawableHoldNote(h));
+ });
+ AddStep("Hold key", () =>
+ {
+ clock.CurrentTime = 0;
+ note.OnPressed(ManiaAction.Key1);
+ });
+ AddStep("progress time", () => clock.CurrentTime = 500);
+ AddAssert("head is visible", () => note.Head.Alpha == 1);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 668487f673..471dad87d5 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -5,13 +5,11 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
-using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects;
@@ -414,14 +412,7 @@ namespace osu.Game.Rulesets.Mania.Tests
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
- AddUntilStep("wait for head", () => currentPlayer.GameplayClockContainer.GameplayClock.CurrentTime >= time_head);
- AddAssert("head is visible",
- () => currentPlayer.ChildrenOfType()
- .Single(note => note.HitObject == beatmap.HitObjects[0])
- .Head
- .Alpha == 1);
-
- AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
+ AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor?.HasCompleted.Value == true);
}
private class ScoreAccessibleReplayPlayer : ReplayPlayer
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs
index 0d726e1a50..ea57e51d1c 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs
@@ -11,7 +11,7 @@ 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)));
+ protected override IResourceStore RulesetResources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneManiaHitObjectSamples)));
///
/// Tests that when a normal sample bank is used, the normal hitsound will be looked up.
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 8f8b99b092..6df555617b 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -2,9 +2,9 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
index 0b58d1efc6..628d77107f 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
@@ -7,7 +7,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
public class ManiaDifficultyAttributes : DifficultyAttributes
{
- public double GreatHitWindow;
- public double ScoreMultiplier;
+ public double GreatHitWindow { get; set; }
+ public double ScoreMultiplier { get; set; }
}
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
index 8c0b9ed8b7..a7a6677b68 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
@@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
// Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
protected override IEnumerable SortObjects(IEnumerable input) => input;
- protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
{
new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns)
};
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
index 00bec18a45..b04ff3548f 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Extensions;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@@ -37,15 +36,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
mods = Score.Mods;
scaledScore = Score.TotalScore;
- 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;
+ countPerfect = Score.Statistics.GetValueOrDefault(HitResult.Perfect);
+ countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
+ countGood = Score.Statistics.GetValueOrDefault(HitResult.Good);
+ countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
+ countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
+ countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
IEnumerable scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease);
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs
deleted file mode 100644
index 4e73883de0..0000000000
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs
+++ /dev/null
@@ -1,45 +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.Mania.Edit.Blueprints.Components;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
-
-namespace osu.Game.Rulesets.Mania.Edit.Blueprints
-{
- public class HoldNoteNoteSelectionBlueprint : ManiaSelectionBlueprint
- {
- protected new DrawableHoldNote DrawableObject => (DrawableHoldNote)base.DrawableObject;
-
- private readonly HoldNotePosition position;
-
- public HoldNoteNoteSelectionBlueprint(DrawableHoldNote holdNote, HoldNotePosition position)
- : base(holdNote)
- {
- this.position = position;
- InternalChild = new EditNotePiece { RelativeSizeAxes = Axes.X };
-
- Select();
- }
-
- protected override void Update()
- {
- base.Update();
-
- // Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
- if (DrawableObject.IsLoaded)
- {
- DrawableNote note = position == HoldNotePosition.Start ? (DrawableNote)DrawableObject.Head : DrawableObject.Tail;
-
- Anchor = note.Anchor;
- Origin = note.Origin;
-
- Size = note.DrawSize;
- Position = note.DrawPosition;
- }
- }
-
- // Todo: This is temporary, since the note masks don't do anything special yet. In the future they will handle input.
- public override bool HandlePositionalInput => false;
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
index 1737c4d2e5..5259fcbd5f 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
@@ -2,28 +2,27 @@
// 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.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
- public class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint
+ public class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint
{
- public new DrawableHoldNote DrawableObject => (DrawableHoldNote)base.DrawableObject;
-
- private readonly IBindable direction = new Bindable();
-
[Resolved]
private OsuColour colours { get; set; }
- public HoldNoteSelectionBlueprint(DrawableHoldNote hold)
+ private EditNotePiece head;
+ private EditNotePiece tail;
+
+ public HoldNoteSelectionBlueprint(HoldNote hold)
: base(hold)
{
}
@@ -31,17 +30,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
- direction.BindTo(scrollingInfo.Direction);
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
InternalChildren = new Drawable[]
{
- new HoldNoteNoteSelectionBlueprint(DrawableObject, HoldNotePosition.Start),
- new HoldNoteNoteSelectionBlueprint(DrawableObject, HoldNotePosition.End),
+ head = new EditNotePiece { RelativeSizeAxes = Axes.X },
+ tail = new EditNotePiece { RelativeSizeAxes = Axes.X },
new Container
{
RelativeSizeAxes = Axes.Both,
@@ -62,21 +54,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.Update();
- // Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
- if (DrawableObject.IsLoaded)
- {
- Size = DrawableObject.DrawSize + new Vector2(0, DrawableObject.Tail.DrawHeight);
-
- // This is a side-effect of not matching the hitobject's anchors/origins, which is kinda hard to do
- // When scrolling upwards our origin is already at the top of the head note (which is the intended location),
- // but when scrolling downwards our origin is at the _bottom_ of the tail note (where we need to be at the _top_ of the tail note)
- if (direction.Value == ScrollingDirection.Down)
- Y -= DrawableObject.Tail.DrawHeight;
- }
+ head.Y = HitObjectContainer.PositionAtTime(HitObject.Head.StartTime, HitObject.StartTime);
+ tail.Y = HitObjectContainer.PositionAtTime(HitObject.Tail.StartTime, HitObject.StartTime);
+ Height = HitObjectContainer.LengthAtTime(HitObject.StartTime, HitObject.EndTime) + tail.DrawHeight;
}
public override Quad SelectionQuad => ScreenSpaceDrawQuad;
- public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre;
+ public override Vector2 ScreenSpaceSelectionPoint => head.ScreenSpaceDrawQuad.Centre;
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
index 384f49d9b2..955336db57 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
@@ -4,22 +4,26 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
-using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
- public abstract class ManiaSelectionBlueprint : OverlaySelectionBlueprint
+ public abstract class ManiaSelectionBlueprint : HitObjectSelectionBlueprint
+ where T : ManiaHitObject
{
- public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject;
+ [Resolved]
+ private Playfield playfield { get; set; }
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
- protected ManiaSelectionBlueprint(DrawableHitObject drawableObject)
- : base(drawableObject)
+ protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)playfield).GetColumn(HitObject.Column).HitObjectContainer;
+
+ protected ManiaSelectionBlueprint(T hitObject)
+ : base(hitObject)
{
RelativeSizeAxes = Axes.None;
}
@@ -28,19 +32,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.Update();
- Position = Parent.ToLocalSpace(DrawableObject.ToScreenSpace(Vector2.Zero));
- }
+ var anchor = scrollingInfo.Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
+ Anchor = Origin = anchor;
+ foreach (var child in InternalChildren)
+ child.Anchor = child.Origin = anchor;
- public override void Show()
- {
- DrawableObject.AlwaysAlive = true;
- base.Show();
- }
-
- public override void Hide()
- {
- DrawableObject.AlwaysAlive = false;
- base.Hide();
+ Position = Parent.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition;
+ Width = HitObjectContainer.DrawWidth;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
index 2bff33c4cf..e7a03905d2 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
@@ -3,25 +3,16 @@
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Objects;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
- public class NoteSelectionBlueprint : ManiaSelectionBlueprint
+ public class NoteSelectionBlueprint : ManiaSelectionBlueprint
{
- public NoteSelectionBlueprint(DrawableNote note)
+ public NoteSelectionBlueprint(Note note)
: base(note)
{
AddInternal(new EditNotePiece { RelativeSizeAxes = Axes.X });
}
-
- protected override void Update()
- {
- base.Update();
-
- // Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
- if (DrawableObject.IsLoaded)
- Size = DrawableObject.DrawSize;
- }
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
index afc08dcc96..9d1f5429a1 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
@@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Edit
foreach (var line in grid.Objects.OfType())
availableLines.Push(line);
- grid.Clear(false);
+ grid.Clear();
}
if (selectionTimeRange == null)
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
index c4429176d1..c5a109a6d1 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
@@ -3,9 +3,8 @@
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Mania.Edit
@@ -17,18 +16,18 @@ namespace osu.Game.Rulesets.Mania.Edit
{
}
- public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject)
+ public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject)
{
switch (hitObject)
{
- case DrawableNote note:
+ case Note note:
return new NoteSelectionBlueprint(note);
- case DrawableHoldNote holdNote:
+ case HoldNote holdNote:
return new HoldNoteSelectionBlueprint(holdNote);
}
- return base.CreateBlueprintFor(hitObject);
+ return base.CreateHitObjectBlueprintFor(hitObject);
}
protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler();
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index 7042110423..dc858fb54f 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
@@ -5,7 +5,6 @@ using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI.Scrolling;
@@ -23,8 +22,8 @@ namespace osu.Game.Rulesets.Mania.Edit
public override bool HandleMovement(MoveSelectionEvent moveEvent)
{
- var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint;
- int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column;
+ var hitObjectBlueprint = (HitObjectSelectionBlueprint)moveEvent.Blueprint;
+ int lastColumn = ((ManiaHitObject)hitObjectBlueprint.Item).Column;
performColumnMovement(lastColumn, moveEvent);
@@ -59,8 +58,9 @@ namespace osu.Game.Rulesets.Mania.Edit
EditorBeatmap.PerformOnSelection(h =>
{
- if (h is ManiaHitObject maniaObj)
- maniaObj.Column += columnDelta;
+ maniaPlayfield.Remove(h);
+ ((ManiaHitObject)h).Column += columnDelta;
+ maniaPlayfield.Add(h);
});
}
}
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index b3889bc7d3..fe736766d9 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
- public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new DrainingHealthProcessor(drainStartTime, 0.2);
+ public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new DrainingHealthProcessor(drainStartTime, 0.5);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mania
public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this);
- public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new ManiaLegacySkinTransformer(source, beatmap);
+ public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new ManiaLegacySkinTransformer(skin, beatmap);
public override IEnumerable ConvertFromLegacyMods(LegacyMods mods)
{
diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
index 1c89d9cd00..36fa336d0c 100644
--- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mania.Configuration;
@@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mania
{
public class ManiaSettingsSubsection : RulesetSettingsSubsection
{
- protected override string Header => "osu!mania";
+ protected override LocalisableString Header => "osu!mania";
public ManiaSettingsSubsection(ManiaRuleset ruleset)
: base(ruleset)
@@ -47,7 +48,7 @@ namespace osu.Game.Rulesets.Mania
private class TimeSlider : OsuSliderBar
{
- public override string TooltipText => Current.Value.ToString("N0") + "ms";
+ public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms";
}
}
}
diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
index f078345fc1..9aebf51576 100644
--- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
@@ -9,12 +9,6 @@ namespace osu.Game.Rulesets.Mania
{
public class ManiaSkinComponent : GameplaySkinComponent
{
- ///
- /// The intended index for this component.
- /// May be null if the component does not exist in a .
- ///
- public readonly int? TargetColumn;
-
///
/// The intended for this component.
/// May be null if the component is not a direct member of a .
@@ -25,12 +19,10 @@ namespace osu.Game.Rulesets.Mania
/// Creates a new .
///
/// The component.
- /// The intended index for this component. May be null if the component does not exist in a .
/// The intended for this component. May be null if the component is not a direct member of a .
- public ManiaSkinComponent(ManiaSkinComponents component, int? targetColumn = null, StageDefinition? stageDefinition = null)
+ public ManiaSkinComponent(ManiaSkinComponents component, StageDefinition? stageDefinition = null)
: base(component)
{
- TargetColumn = targetColumn;
StageDefinition = stageDefinition;
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
index 8fd5950dfb..050b302bd8 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
@@ -15,7 +15,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public abstract int KeyCount { get; }
public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1; // TODO: Implement the mania key mod score multiplier
- public override bool Ranked => true;
public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
{
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
index 105d88129c..6ae854e7f3 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
@@ -4,7 +4,6 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
-using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
@@ -12,7 +11,7 @@ using osu.Game.Users;
namespace osu.Game.Rulesets.Mania.Mods
{
- public class ManiaModAutoplay : ModAutoplay
+ public class ManiaModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs
index 078394b1d8..614ef76a3b 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs
@@ -24,8 +24,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override ModType Type => ModType.Conversion;
- public override bool Ranked => false;
-
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
var maniaRuleset = (DrawableManiaRuleset)drawableRuleset;
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
index 12f379bddb..cf404cc98e 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
@@ -17,7 +17,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override ModType Type => ModType.Conversion;
public override string Description => "Notes are flipped horizontally.";
public override double ScoreMultiplier => 1;
- public override bool Ranked => true;
public void ApplyToBeatmap(IBeatmap beatmap)
{
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs
index 87501d07a5..3c24e91d54 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.Mods
@@ -39,5 +40,13 @@ namespace osu.Game.Rulesets.Mania.Mods
}));
}
}
+
+ protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
+ {
+ }
+
+ protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
+ {
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs
index 699c58c373..6f2d4fe91e 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Utils;
@@ -17,8 +18,11 @@ namespace osu.Game.Rulesets.Mania.Mods
public void ApplyToBeatmap(IBeatmap beatmap)
{
+ Seed.Value ??= RNG.Next();
+ var rng = new Random((int)Seed.Value);
+
var availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
- var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(item => RNG.Next()).ToList();
+ var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(item => rng.Next()).ToList();
beatmap.HitObjects.OfType().ForEach(h => h.Column = shuffledColumns[h.Column]);
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 02829d87bd..d1310d42eb 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -12,6 +13,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
@@ -29,21 +31,21 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public DrawableHoldNoteHead Head => headContainer.Child;
public DrawableHoldNoteTail Tail => tailContainer.Child;
- private readonly Container headContainer;
- private readonly Container tailContainer;
- private readonly Container tickContainer;
+ private Container headContainer;
+ private Container tailContainer;
+ private Container tickContainer;
///
/// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed.
///
- private readonly Container sizingContainer;
+ private Container sizingContainer;
///
/// Contains the contents of the hold note that should be masked as the hold note is being pressed. Follows changes in the size of .
///
- private readonly Container maskingContainer;
+ private Container maskingContainer;
- private readonly SkinnableDrawable bodyPiece;
+ private SkinnableDrawable bodyPiece;
///
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
@@ -60,11 +62,19 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
private double? releaseTime;
+ public DrawableHoldNote()
+ : this(null)
+ {
+ }
+
public DrawableHoldNote(HoldNote hitObject)
: base(hitObject)
{
- RelativeSizeAxes = Axes.X;
+ }
+ [BackgroundDependencyLoader]
+ private void load()
+ {
Container maskedContents;
AddRangeInternal(new Drawable[]
@@ -86,7 +96,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
headContainer = new Container { RelativeSizeAxes = Axes.Both }
}
},
- bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece
+ bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody), _ => new DefaultBodyPiece
{
RelativeSizeAxes = Axes.Both,
})
@@ -105,6 +115,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
});
}
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ sizingContainer.Size = Vector2.One;
+ HoldStartTime = null;
+ HoldBrokenTime = null;
+ releaseTime = null;
+ }
+
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);
@@ -128,37 +148,23 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
- headContainer.Clear();
- tailContainer.Clear();
- tickContainer.Clear();
+ headContainer.Clear(false);
+ tailContainer.Clear(false);
+ tickContainer.Clear(false);
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
{
switch (hitObject)
{
- case TailNote _:
- return new DrawableHoldNoteTail(this)
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- AccentColour = { BindTarget = AccentColour }
- };
+ case TailNote tail:
+ return new DrawableHoldNoteTail(tail);
- case Note _:
- return new DrawableHoldNoteHead(this)
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- AccentColour = { BindTarget = AccentColour }
- };
+ case HeadNote head:
+ return new DrawableHoldNoteHead(head);
case HoldNoteTick tick:
- return new DrawableHoldNoteTick(tick)
- {
- HoldStartTime = () => HoldStartTime,
- AccentColour = { BindTarget = AccentColour }
- };
+ return new DrawableHoldNoteTick(tick);
}
return base.CreateNestedHitObject(hitObject);
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
index 35ba2465fa..be600f0d47 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
@@ -12,11 +13,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteHead;
- public DrawableHoldNoteHead(DrawableHoldNote holdNote)
- : base(holdNote.HitObject.Head)
+ public DrawableHoldNoteHead()
+ : this(null)
{
}
+ public DrawableHoldNoteHead(HeadNote headNote)
+ : base(headNote)
+ {
+ Anchor = Anchor.TopCentre;
+ Origin = Anchor.TopCentre;
+ }
+
public void UpdateResult() => base.UpdateResult(true);
protected override void UpdateInitialTransforms()
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
index 3a00933e4d..18aa3f66d4 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
+using osu.Framework.Graphics;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
@@ -20,12 +21,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail;
- private readonly DrawableHoldNote holdNote;
+ protected DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject;
- public DrawableHoldNoteTail(DrawableHoldNote holdNote)
- : base(holdNote.HitObject.Tail)
+ public DrawableHoldNoteTail()
+ : this(null)
{
- this.holdNote = holdNote;
+ }
+
+ public DrawableHoldNoteTail(TailNote tailNote)
+ : base(tailNote)
+ {
+ Anchor = Anchor.TopCentre;
+ Origin = Anchor.TopCentre;
}
public void UpdateResult() => base.UpdateResult(true);
@@ -54,7 +61,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
ApplyResult(r =>
{
// If the head wasn't hit or the hold note was broken, cap the max score to Meh.
- if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HoldBrokenTime != null))
+ if (result > HitResult.Meh && (!HoldNote.Head.IsHit || HoldNote.HoldBrokenTime != null))
result = HitResult.Meh;
r.Type = result;
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
index 98931dceed..f040dad135 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
@@ -2,7 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osuTK;
+using System.Diagnostics;
+using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -19,38 +20,48 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
/// References the time at which the user started holding the hold note.
///
- public Func HoldStartTime;
+ private Func holdStartTime;
+
+ private Container glowContainer;
+
+ public DrawableHoldNoteTick()
+ : this(null)
+ {
+ }
public DrawableHoldNoteTick(HoldNoteTick hitObject)
: base(hitObject)
{
- Container glowContainer;
-
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
RelativeSizeAxes = Axes.X;
- Size = new Vector2(1);
+ }
- AddRangeInternal(new[]
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddInternal(glowContainer = new CircularContainer
{
- glowContainer = new CircularContainer
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Children = new[]
{
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- Children = new[]
+ new Box
{
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- AlwaysPresent = true
- }
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true
}
}
});
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
AccentColour.BindValueChanged(colour =>
{
@@ -64,12 +75,29 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}, true);
}
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ Debug.Assert(ParentHitObject != null);
+
+ var holdNote = (DrawableHoldNote)ParentHitObject;
+ holdStartTime = () => holdNote.HoldStartTime;
+ }
+
+ protected override void OnFree()
+ {
+ base.OnFree();
+
+ holdStartTime = null;
+ }
+
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (Time.Current < HitObject.StartTime)
return;
- var startTime = HoldStartTime?.Invoke();
+ var startTime = holdStartTime?.Invoke();
if (startTime == null || startTime > HitObject.StartTime)
ApplyResult(r => r.Type = r.Judgement.MinResult);
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index 1550faee50..5aff4e200b 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -6,6 +6,7 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.Mania.UI;
@@ -21,9 +22,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected readonly IBindable Direction = new Bindable();
+ // Leaving the default (10s) makes hitobjects not appear, as this offset is used for the initial state transforms.
+ // Calculated as DrawableManiaRuleset.MAX_TIME_RANGE + some additional allowance for velocity < 1.
+ protected override double InitialLifetimeOffset => 30000;
+
[Resolved(canBeNull: true)]
private ManiaPlayfield playfield { get; set; }
+ ///
+ /// Gets the samples that are played by this object during gameplay.
+ ///
+ public ISampleInfo[] GetGameplaySamples() => Samples.Samples;
+
protected override float SamplePlaybackPosition
{
get
@@ -44,6 +54,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected DrawableManiaHitObject(ManiaHitObject hitObject)
: base(hitObject)
{
+ RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader(true)]
@@ -53,64 +64,29 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Action.BindTo(action);
Direction.BindTo(scrollingInfo.Direction);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
Direction.BindValueChanged(OnDirectionChanged, true);
}
- private double computedLifetimeStart;
-
- public override double LifetimeStart
+ protected override void OnApply()
{
- get => base.LifetimeStart;
- set
- {
- computedLifetimeStart = value;
+ base.OnApply();
- if (!AlwaysAlive)
- base.LifetimeStart = value;
- }
+ if (ParentHitObject != null)
+ AccentColour.BindTo(ParentHitObject.AccentColour);
}
- private double computedLifetimeEnd;
-
- public override double LifetimeEnd
+ protected override void OnFree()
{
- get => base.LifetimeEnd;
- set
- {
- computedLifetimeEnd = value;
+ base.OnFree();
- 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;
- }
- }
+ if (ParentHitObject != null)
+ AccentColour.UnbindFrom(ParentHitObject.AccentColour);
}
protected virtual void OnDirectionChanged(ValueChangedEvent e)
@@ -141,12 +117,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public abstract class DrawableManiaHitObject : DrawableManiaHitObject
where TObject : ManiaHitObject
{
- public new readonly TObject HitObject;
+ public new TObject HitObject => (TObject)base.HitObject;
protected DrawableManiaHitObject(TObject hitObject)
: base(hitObject)
{
- HitObject = hitObject;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
index 36565e14aa..33d872dfb6 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
@@ -33,31 +33,37 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected virtual ManiaSkinComponents Component => ManiaSkinComponents.Note;
- private readonly Drawable headPiece;
+ private Drawable headPiece;
+
+ public DrawableNote()
+ : this(null)
+ {
+ }
public DrawableNote(Note hitObject)
: base(hitObject)
{
- RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
-
- AddInternal(headPiece = new SkinnableDrawable(new ManiaSkinComponent(Component, hitObject.Column), _ => new DefaultNotePiece())
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y
- });
}
[BackgroundDependencyLoader(true)]
private void load(ManiaRulesetConfigManager rulesetConfig)
{
rulesetConfig?.BindWith(ManiaRulesetSetting.TimingBasedNoteColouring, configTimingBasedNoteColouring);
+
+ AddInternal(headPiece = new SkinnableDrawable(new ManiaSkinComponent(Component), _ => new DefaultNotePiece())
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ });
}
protected override void LoadComplete()
{
- HitObject.StartTimeBindable.BindValueChanged(_ => updateSnapColour());
- configTimingBasedNoteColouring.BindValueChanged(_ => updateSnapColour(), true);
+ base.LoadComplete();
+
+ configTimingBasedNoteColouring.BindValueChanged(_ => updateSnapColour());
+ StartTimeBindable.BindValueChanged(_ => updateSnapColour(), true);
}
protected override void OnDirectionChanged(ValueChangedEvent e)
@@ -102,7 +108,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
private void updateSnapColour()
{
- if (beatmap == null) return;
+ if (beatmap == null || HitObject == null) return;
int snapDivisor = beatmap.ControlPointInfo.GetClosestBeatDivisor(HitObject.StartTime);
diff --git a/osu.Game.Rulesets.Mania/Objects/HeadNote.cs b/osu.Game.Rulesets.Mania/Objects/HeadNote.cs
new file mode 100644
index 0000000000..e69cc62aed
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Objects/HeadNote.cs
@@ -0,0 +1,9 @@
+// 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.Objects
+{
+ public class HeadNote : Note
+ {
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
index 6cc7ff92d3..43e876b7aa 100644
--- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Mania.Objects
///
/// The head note of the hold.
///
- public Note Head { get; private set; }
+ public HeadNote Head { get; private set; }
///
/// The tail note of the hold.
@@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.Objects
createTicks(cancellationToken);
- AddNested(Head = new Note
+ AddNested(Head = new HeadNote
{
StartTime = StartTime,
Column = Column,
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
index ada84dfac2..517b708691 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
-using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
@@ -11,7 +10,7 @@ using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Mania.Replays
{
- internal class ManiaAutoGenerator : AutoGenerator
+ internal class ManiaAutoGenerator : AutoGenerator
{
public const double RELEASE_DELAY = 20;
@@ -22,8 +21,6 @@ namespace osu.Game.Rulesets.Mania.Replays
public ManiaAutoGenerator(ManiaBeatmap beatmap)
: base(beatmap)
{
- Replay = new Replay();
-
columnActions = new ManiaAction[Beatmap.TotalColumns];
var normalAction = ManiaAction.Key1;
@@ -43,12 +40,10 @@ namespace osu.Game.Rulesets.Mania.Replays
}
}
- protected Replay Replay;
-
- public override Replay Generate()
+ protected override void GenerateFrames()
{
if (Beatmap.HitObjects.Count == 0)
- return Replay;
+ return;
var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time);
@@ -70,10 +65,8 @@ namespace osu.Game.Rulesets.Mania.Replays
}
}
- Replay.Frames.Add(new ManiaReplayFrame(group.First().Time, actions.ToArray()));
+ Frames.Add(new ManiaReplayFrame(group.First().Time, actions.ToArray()));
}
-
- return Replay;
}
private IEnumerable generateActionPoints()
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
index 24ccae895d..814a737034 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
@@ -50,29 +50,25 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{ HitResult.Miss, "mania-hit0" }
};
- private Lazy isLegacySkin;
+ private readonly Lazy isLegacySkin;
///
/// Whether texture for the keys exists.
/// Used to determine if the mania ruleset is skinned.
///
- private Lazy hasKeyTexture;
+ private readonly Lazy hasKeyTexture;
- public ManiaLegacySkinTransformer(ISkinSource source, IBeatmap beatmap)
- : base(source)
+ public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap)
+ : base(skin)
{
this.beatmap = (ManiaBeatmap)beatmap;
- Source.SourceChanged += sourceChanged;
- sourceChanged();
- }
-
- private void sourceChanged()
- {
- isLegacySkin = new Lazy