diff --git a/.editorconfig b/.editorconfig
index 67f98f94eb..a5f7795882 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -191,4 +191,7 @@ dotnet_diagnostic.IDE0052.severity = silent
#Rules for disposable
dotnet_diagnostic.IDE0067.severity = none
dotnet_diagnostic.IDE0068.severity = none
-dotnet_diagnostic.IDE0069.severity = none
\ No newline at end of file
+dotnet_diagnostic.IDE0069.severity = none
+
+#Disable operator overloads requiring alternate named methods
+dotnet_diagnostic.CA2225.severity = none
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
index 2cd40c8675..2d3478f256 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -16,7 +16,7 @@
-
+
diff --git a/Gemfile.lock b/Gemfile.lock
index bf971d2c22..a4b49af7e4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -6,35 +6,36 @@ GEM
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
aws-eventstream (1.1.0)
- aws-partitions (1.329.0)
- aws-sdk-core (3.99.2)
+ aws-partitions (1.354.0)
+ aws-sdk-core (3.104.3)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
- aws-sdk-kms (1.34.1)
+ aws-sdk-kms (1.36.0)
aws-sdk-core (~> 3, >= 3.99.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.68.1)
- aws-sdk-core (~> 3, >= 3.99.0)
+ aws-sdk-s3 (1.78.0)
+ aws-sdk-core (~> 3, >= 3.104.3)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
- aws-sigv4 (1.1.4)
- aws-eventstream (~> 1.0, >= 1.0.2)
+ aws-sigv4 (1.2.1)
+ aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.3)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
- declarative (0.0.10)
+ declarative (0.0.20)
declarative-option (0.1.0)
- digest-crc (0.5.1)
+ digest-crc (0.6.1)
+ rake (~> 13.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
- dotenv (2.7.5)
- emoji_regex (1.0.1)
- excon (0.74.0)
+ dotenv (2.7.6)
+ emoji_regex (3.0.0)
+ excon (0.76.0)
faraday (1.0.1)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
@@ -42,34 +43,32 @@ GEM
http-cookie (~> 1.0.0)
faraday_middleware (1.0.0)
faraday (~> 1.0)
- fastimage (2.1.7)
- fastlane (2.149.1)
+ fastimage (2.2.0)
+ fastlane (2.156.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
aws-sdk-s3 (~> 1.0)
- babosa (>= 1.0.2, < 2.0.0)
+ babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0)
- emoji_regex (>= 0.1, < 2.0)
+ emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
- faraday (>= 0.17, < 2.0)
+ faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
- faraday_middleware (>= 0.13.1, < 2.0)
+ faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.37.0, < 0.39.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
json (< 3.0.0)
- jwt (~> 2.1.0)
+ jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
- multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
plist (>= 3.1.0, < 4.0.0)
- public_suffix (~> 2.0.0)
- rubyzip (>= 1.3.0, < 2.0.0)
+ rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
@@ -97,17 +96,17 @@ GEM
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
- google-cloud-env (1.3.2)
+ google-cloud-env (1.3.3)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.1)
- google-cloud-storage (1.26.2)
+ google-cloud-storage (1.27.0)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
- googleauth (0.12.0)
+ googleauth (0.13.1)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
@@ -119,29 +118,29 @@ GEM
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.4.0)
- json (2.3.0)
- jwt (2.1.0)
+ json (2.3.1)
+ jwt (2.2.1)
memoist (0.16.2)
mini_magick (4.10.1)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
- multi_json (1.14.1)
- multi_xml (0.6.0)
+ multi_json (1.15.0)
multipart-post (2.0.0)
- nanaimo (0.2.6)
+ nanaimo (0.3.0)
naturally (2.2.0)
- nokogiri (1.10.7)
+ nokogiri (1.10.10)
mini_portile2 (~> 2.4.0)
- os (1.1.0)
+ os (1.1.1)
plist (3.5.0)
- public_suffix (2.0.5)
+ public_suffix (4.0.5)
+ rake (13.0.1)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rouge (2.0.7)
- rubyzip (1.3.0)
+ rubyzip (2.3.0)
security (0.1.3)
signet (0.14.0)
addressable (~> 2.3)
@@ -160,7 +159,7 @@ GEM
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
tty-cursor (0.7.1)
- tty-screen (0.8.0)
+ tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
@@ -169,12 +168,12 @@ GEM
unf_ext (0.0.7.7)
unicode-display_width (1.7.0)
word_wrap (1.0.0)
- xcodeproj (1.16.0)
+ xcodeproj (1.18.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
- nanaimo (~> 0.2.6)
+ nanaimo (~> 0.3.0)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0)
diff --git a/README.md b/README.md
index dc3ee63844..7c749f3422 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,9 @@
[![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu)
[![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
-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.
+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.
## Status
@@ -36,7 +38,13 @@ If you are looking to install or test osu! without setting up a development envi
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
-## Developing or debugging
+## Developing a custom ruleset
+
+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-templates).
+
+You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/issues/5852).
+
+## Developing osu!
Please make sure you have the following prerequisites:
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 4fd0e5e8c7..8c278604aa 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -113,7 +113,7 @@ platform :ios do
souyuz(
platform: "ios",
- plist_path: "../osu.iOS/Info.plist"
+ plist_path: "osu.iOS/Info.plist"
)
end
@@ -127,7 +127,7 @@ platform :ios do
end
lane :update_version do |options|
- options[:plist_path] = '../osu.iOS/Info.plist'
+ options[:plist_path] = 'osu.iOS/Info.plist'
app_version(options)
end
diff --git a/global.json b/global.json
index 233a040d18..a9a531f59c 100644
--- a/global.json
+++ b/global.json
@@ -5,6 +5,6 @@
"version": "3.1.100"
},
"msbuild-sdks": {
- "Microsoft.Build.Traversal": "2.0.52"
+ "Microsoft.Build.Traversal": "2.1.1"
}
}
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index e5fed09c07..d7817cf4cf 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
-
+
+
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index 9839d16030..db73bb7e7f 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -9,7 +9,7 @@ using osu.Framework.Android;
namespace osu.Android
{
- [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
+ [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
public class OsuGameActivity : AndroidGameActivity
{
protected override Framework.Game CreateGame() => new OsuGameAndroid();
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 7a99c70999..62e8f7c518 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -3,7 +3,7 @@
netcoreapp3.1
WinExe
true
- click the circles. to the beat.
+ A free-to-win rhythm game. Rhythm is just a *click* away!
osu!
osu!lazer
osu!lazer
diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec
index a919d54f38..2fc6009183 100644
--- a/osu.Desktop/osu.nuspec
+++ b/osu.Desktop/osu.nuspec
@@ -9,8 +9,7 @@
https://osu.ppy.sh/
https://puu.sh/tYyXZ/9a01a5d1b0.ico
false
- click the circles. to the beat.
- click the circles.
+ A free-to-win rhythm game. Rhythm is just a *click* away!
testing
Copyright (c) 2020 ppy Pty Ltd
en-AU
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
index df54df7b01..466cbdaf8d 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
@@ -14,6 +14,7 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
+ [Timeout(10000)]
public class CatchBeatmapConversionTest : BeatmapConversionTest
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
@@ -25,6 +26,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase("hardrock-stream", new[] { typeof(CatchModHardRock) })]
[TestCase("hardrock-repeat-slider", new[] { typeof(CatchModHardRock) })]
[TestCase("hardrock-spinner", new[] { typeof(CatchModHardRock) })]
+ [TestCase("right-bound-hr-offset", new[] { typeof(CatchModHardRock) })]
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
protected override IEnumerable CreateConvertValue(HitObject hitObject)
diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
index c1b7214d72..3e06e78dba 100644
--- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Droplet { StartTime = 1000 }), shouldMiss);
// We only care about testing misses, hits are tested via JuiceStream
- [TestCase(false)]
+ [TestCase(true)]
public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss);
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs
new file mode 100644
index 0000000000..1eb0975010
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs
@@ -0,0 +1,84 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Mods;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Tests.Mods
+{
+ public class TestSceneCatchModRelax : ModTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+
+ [Test]
+ public void TestModRelax() => CreateModTest(new ModTestData
+ {
+ Mod = new CatchModRelax(),
+ Autoplay = false,
+ PassCondition = passCondition,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Fruit
+ {
+ X = CatchPlayfield.CENTER_X,
+ StartTime = 0
+ },
+ new Fruit
+ {
+ X = 0,
+ StartTime = 250
+ },
+ new Fruit
+ {
+ X = CatchPlayfield.WIDTH,
+ StartTime = 500
+ },
+ new JuiceStream
+ {
+ X = CatchPlayfield.CENTER_X,
+ StartTime = 750,
+ Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 })
+ }
+ }
+ }
+ });
+
+ private bool passCondition()
+ {
+ var playfield = this.ChildrenOfType().Single();
+
+ switch (Player.ScoreProcessor.Combo.Value)
+ {
+ case 0:
+ InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre);
+ break;
+
+ case 1:
+ InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.BottomLeft);
+ break;
+
+ case 2:
+ InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.BottomRight);
+ break;
+
+ case 3:
+ InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre);
+ break;
+ }
+
+ return Player.ScoreProcessor.Combo.Value >= 6;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png
new file mode 100644
index 0000000000..8304617d8c
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-1.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-1.png
new file mode 100644
index 0000000000..c3b85eb873
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-1.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-2.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-2.png
new file mode 100644
index 0000000000..7f65eb7ca7
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-2.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png
new file mode 100644
index 0000000000..82bec3babe
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-4.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-4.png
new file mode 100644
index 0000000000..5e38c75a9d
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-4.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-5.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-5.png
new file mode 100644
index 0000000000..a562d9f2ac
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-5.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png
new file mode 100644
index 0000000000..b4cf81f26e
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png
new file mode 100644
index 0000000000..a23f5379b2
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-8.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-8.png
new file mode 100644
index 0000000000..430b18509d
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-8.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-9.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-9.png
new file mode 100644
index 0000000000..add1202c31
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-9.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-0@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-0@2x.png
new file mode 100644
index 0000000000..508cc85e4a
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-0@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-1@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-1@2x.png
new file mode 100644
index 0000000000..84f74e1ec9
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-1@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-2@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-2@2x.png
new file mode 100644
index 0000000000..49625c6623
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-2@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-3@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-3@2x.png
new file mode 100644
index 0000000000..623b24612f
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-3@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-4@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-4@2x.png
new file mode 100644
index 0000000000..a33286dc8f
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-4@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-5@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-5@2x.png
new file mode 100644
index 0000000000..d8250b0c63
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-5@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-6@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-6@2x.png
new file mode 100644
index 0000000000..75d3cbd3bd
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-6@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-7@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-7@2x.png
new file mode 100644
index 0000000000..cfe2021df4
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-7@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-8@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-8@2x.png
new file mode 100644
index 0000000000..ba9492c7f8
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-8@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-9@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-9@2x.png
new file mode 100644
index 0000000000..a7b6b81570
Binary files /dev/null and b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-9@2x.png differ
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
index d6bba3d55e..3c636a5b97 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
@@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using System.Linq;
+using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
@@ -38,7 +40,11 @@ namespace osu.Game.Rulesets.Catch.Tests
new Vector2(width, 0)
}),
StartTime = i * 2000,
- NewCombo = i % 8 == 0
+ NewCombo = i % 8 == 0,
+ Samples = new List(new[]
+ {
+ new HitSampleInfo { Bank = "normal", Name = "hitnormal", Volume = 100 }
+ })
});
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index b4f123598b..e055f08dc2 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Catch.Tests
Schedule(() =>
{
area.AttemptCatch(fruit);
- area.OnResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great });
+ area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great });
drawable.Expire();
});
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
new file mode 100644
index 0000000000..c7b322c8a0
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs
@@ -0,0 +1,65 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneComboCounter : CatchSkinnableTestScene
+ {
+ private ScoreProcessor scoreProcessor;
+
+ private Color4 judgedObjectColour = Color4.White;
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ scoreProcessor = new ScoreProcessor();
+
+ SetContents(() => new CatchComboDisplay
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(2.5f),
+ });
+ });
+
+ [Test]
+ public void TestCatchComboCounter()
+ {
+ AddRepeatStep("perform hit", () => performJudgement(HitResult.Great), 20);
+ AddStep("perform miss", () => performJudgement(HitResult.Miss));
+
+ AddStep("randomize judged object colour", () =>
+ {
+ judgedObjectColour = new Color4(
+ RNG.NextSingle(1f),
+ RNG.NextSingle(1f),
+ RNG.NextSingle(1f),
+ 1f
+ );
+ });
+ }
+
+ private void performJudgement(HitResult type, Judgement judgement = null)
+ {
+ var judgedObject = new DrawableFruit(new Fruit()) { AccentColour = { Value = judgedObjectColour } };
+
+ var result = new JudgementResult(judgedObject.HitObject, judgement ?? new Judgement()) { Type = type };
+ scoreProcessor.ApplyResult(result);
+
+ foreach (var counter in CreatedDrawables.Cast())
+ counter.OnNewResult(judgedObject, result);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
index c07e4fdad3..385d8ed7fa 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
@@ -20,19 +20,19 @@ namespace osu.Game.Rulesets.Catch.Tests
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
AddStep($"show {rep}", () => SetContents(() => createDrawable(rep)));
- AddStep("show droplet", () => SetContents(createDrawableDroplet));
-
+ AddStep("show droplet", () => SetContents(() => createDrawableDroplet()));
AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet));
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawable(rep, true)));
+
+ AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true)));
}
private Drawable createDrawableTinyDroplet()
{
- var droplet = new TinyDroplet
+ var droplet = new TestCatchTinyDroplet
{
- StartTime = Clock.CurrentTime,
Scale = 1.5f,
};
@@ -47,12 +47,12 @@ namespace osu.Game.Rulesets.Catch.Tests
};
}
- private Drawable createDrawableDroplet()
+ private Drawable createDrawableDroplet(bool hyperdash = false)
{
- var droplet = new Droplet
+ var droplet = new TestCatchDroplet
{
- StartTime = Clock.CurrentTime,
Scale = 1.5f,
+ HyperDashTarget = hyperdash ? new Banana() : null
};
return new DrawableDroplet(droplet)
@@ -95,5 +95,21 @@ namespace osu.Game.Rulesets.Catch.Tests
public override FruitVisualRepresentation VisualRepresentation { get; }
}
+
+ public class TestCatchDroplet : Droplet
+ {
+ public TestCatchDroplet()
+ {
+ StartTime = 1000000000000;
+ }
+ }
+
+ public class TestCatchTinyDroplet : TinyDroplet
+ {
+ public TestCatchTinyDroplet()
+ {
+ StartTime = 1000000000000;
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
index ad24adf352..db09b2bc6b 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
@@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
@@ -18,23 +19,43 @@ namespace osu.Game.Rulesets.Catch.Tests
{
protected override bool Autoplay => true;
+ private int hyperDashCount;
+ private bool inHyperDash;
+
[Test]
public void TestHyperDash()
{
- AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash);
- AddUntilStep("wait for right movement", () => getCatcher().Scale.X > 0); // don't check hyperdashing as it happens too fast.
-
- AddUntilStep("wait for left movement", () => getCatcher().Scale.X < 0);
-
- for (int i = 0; i < 3; i++)
+ AddStep("reset count", () =>
{
- AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing);
- AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing);
+ inHyperDash = false;
+ hyperDashCount = 0;
+
+ // this needs to be done within the frame stable context due to how quickly hyperdash state changes occur.
+ Player.DrawableRuleset.FrameStableComponents.OnUpdate += d =>
+ {
+ var catcher = Player.ChildrenOfType().FirstOrDefault()?.MovableCatcher;
+
+ if (catcher == null)
+ return;
+
+ if (catcher.HyperDashing != inHyperDash)
+ {
+ inHyperDash = catcher.HyperDashing;
+ if (catcher.HyperDashing)
+ hyperDashCount++;
+ }
+ };
+ });
+
+ AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash);
+
+ for (int i = 0; i < 9; i++)
+ {
+ int count = i + 1;
+ AddUntilStep($"wait for hyperdash #{count}", () => hyperDashCount >= count);
}
}
- private Catcher getCatcher() => Player.ChildrenOfType().First().MovableCatcher;
-
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap
@@ -46,6 +67,8 @@ namespace osu.Game.Rulesets.Catch.Tests
}
};
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
+
// Should produce a hyper-dash (edge case test)
beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56, NewCombo = true });
beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308, NewCombo = true });
@@ -63,6 +86,20 @@ namespace osu.Game.Rulesets.Catch.Tests
createObjects(() => new Fruit { X = right_x });
createObjects(() => new TestJuiceStream(left_x), 1);
+ beatmap.ControlPointInfo.Add(startTime, new TimingControlPoint
+ {
+ BeatLength = 50
+ });
+
+ createObjects(() => new TestJuiceStream(left_x)
+ {
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(Vector2.Zero),
+ new PathControlPoint(new Vector2(512, 0))
+ })
+ }, 1);
+
return beatmap;
void createObjects(Func createObject, int count = 3)
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 7c0b73e8c3..dfe3bf8af4 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs
index 18cc300ff9..f009c10a9c 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
@@ -23,19 +22,19 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
Name = @"Fruit Count",
Content = fruits.ToString(),
- Icon = FontAwesome.Regular.Circle
+ CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
},
new BeatmapStatistic
{
Name = @"Juice Stream Count",
Content = juiceStreams.ToString(),
- Icon = FontAwesome.Regular.Circle
+ CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
},
new BeatmapStatistic
{
Name = @"Banana Shower Count",
Content = bananaShowers.ToString(),
- Icon = FontAwesome.Regular.Circle
+ CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
}
};
}
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
index 145a40f5f5..34964fc4ae 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
@@ -5,6 +5,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Objects;
using osu.Framework.Extensions.IEnumerableExtensions;
@@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
- protected override IEnumerable ConvertHitObject(HitObject obj, IBeatmap beatmap)
+ protected override IEnumerable ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken)
{
var positionData = obj as IHasXPosition;
var comboData = obj as IHasCombo;
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index bb14988414..a08c5b6fb1 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
if (amount > 0)
{
// Clamp to the right bound
- if (position + amount < 1)
+ if (position + amount < CatchPlayfield.WIDTH)
position += amount;
}
else
@@ -212,6 +212,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2;
+
+ // Todo: This is wrong. osu!stable calculated hyperdashes using the full catcher size, excluding the margins.
+ // This should theoretically cause impossible scenarios, but practically, likely due to the size of the playfield, it doesn't seem possible.
+ // For now, to bring gameplay (and diffcalc!) completely in-line with stable, this code also uses the full catcher size.
+ halfCatcherWidth /= Catcher.ALLOWED_CATCH_RANGE;
+
int lastDirection = 0;
double lastExcess = halfCatcherWidth;
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index 9437023c70..1f27de3352 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -21,13 +21,11 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System;
-using osu.Framework.Testing;
using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
{
- [ExcludeFromDynamicCompile]
public class CatchRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableCatchRuleset(this, beatmap, mods);
@@ -147,7 +145,7 @@ namespace osu.Game.Rulesets.Catch
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);
- public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new CatchPerformanceCalculator(this, beatmap, score);
+ public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score);
public int LegacyID => 2;
diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
index 80390705fe..23d8428fec 100644
--- a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
+++ b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
@@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Catch
Droplet,
CatcherIdle,
CatcherFail,
- CatcherKiai
+ CatcherKiai,
+ CatchComboCounter
}
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
index 75f5b18607..fa9011d826 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
@@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Catch.Difficulty
public class CatchDifficultyAttributes : DifficultyAttributes
{
public double ApproachRate;
- public int MaxCombo;
}
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
index d700f79e5b..6a3a16ed33 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
-using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@@ -25,8 +24,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
private int tinyTicksMissed;
private int misses;
- public CatchPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score)
- : base(ruleset, beatmap, score)
+ public CatchPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
+ : base(ruleset, attributes, score)
{
}
@@ -34,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
mods = Score.Mods;
- fruitsHit = Score.Statistics.GetOrDefault(HitResult.Perfect);
+ fruitsHit = Score.Statistics.GetOrDefault(HitResult.Great);
ticksHit = Score.Statistics.GetOrDefault(HitResult.LargeTickHit);
tinyTicksHit = Score.Statistics.GetOrDefault(HitResult.SmallTickHit);
tinyTicksMissed = Score.Statistics.GetOrDefault(HitResult.SmallTickMiss);
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
index a7449ba4e1..b919102215 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
@@ -8,31 +8,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
{
public class CatchBananaJudgement : CatchJudgement
{
- public override bool AffectsCombo => false;
-
- protected override int NumericResultFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 1100;
- }
- }
-
- protected override double HealthIncreaseFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return DEFAULT_MAX_HEALTH_INCREASE * 0.75;
- }
- }
+ public override HitResult MaxResult => HitResult.LargeBonus;
public override bool ShouldExplodeFor(JudgementResult result) => true;
}
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
index e87ecba749..8fd7b93e4c 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs
@@ -7,16 +7,6 @@ namespace osu.Game.Rulesets.Catch.Judgements
{
public class CatchDropletJudgement : CatchJudgement
{
- protected override int NumericResultFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 30;
- }
- }
+ public override HitResult MaxResult => HitResult.LargeTickHit;
}
}
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
index 2149ed9712..ccafe0abc4 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs
@@ -9,19 +9,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
{
public class CatchJudgement : Judgement
{
- public override HitResult MaxResult => HitResult.Perfect;
-
- protected override int NumericResultFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 300;
- }
- }
+ public override HitResult MaxResult => HitResult.Great;
///
/// Whether fruit on the platter should explode or drop.
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
index d607b49ea4..d957d4171b 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs
@@ -7,30 +7,6 @@ namespace osu.Game.Rulesets.Catch.Judgements
{
public class CatchTinyDropletJudgement : CatchJudgement
{
- public override bool AffectsCombo => false;
-
- protected override int NumericResultFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 10;
- }
- }
-
- protected override double HealthIncreaseFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 0.02;
- }
- }
+ public override HitResult MaxResult => HitResult.SmallTickHit;
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
index c1d24395e4..1e42c6a240 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Mods
protected override bool OnMouseMove(MouseMoveEvent e)
{
- catcher.UpdatePosition(e.MousePosition.X / DrawSize.X);
+ catcher.UpdatePosition(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 04932ecdbb..5985ec9b68 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -27,6 +27,11 @@ namespace osu.Game.Rulesets.Catch.Objects
set => x = value;
}
+ ///
+ /// Whether this object can be placed on the catcher's plate.
+ ///
+ public virtual bool CanBePlated => false;
+
///
/// A random offset applied to , set by the .
///
@@ -100,6 +105,14 @@ namespace osu.Game.Rulesets.Catch.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
+ ///
+ /// Represents a single object that can be caught by the catcher.
+ ///
+ public abstract class PalpableCatchHitObject : CatchHitObject
+ {
+ public override bool CanBePlated => true;
+ }
+
public enum FruitVisualRepresentation
{
Pear,
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
index c6345a9df7..d03a764bda 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
@@ -8,21 +8,18 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Catch.UI;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
- public abstract class PalpableCatchHitObject : DrawableCatchHitObject
- where TObject : CatchHitObject
+ public abstract class PalpableDrawableCatchHitObject : DrawableCatchHitObject
+ where TObject : PalpableCatchHitObject
{
- public override bool CanBePlated => true;
-
protected Container ScaleContainer { get; private set; }
- protected PalpableCatchHitObject(TObject hitObject)
+ protected PalpableDrawableCatchHitObject(TObject hitObject)
: base(hitObject)
{
Origin = Anchor.Centre;
@@ -65,9 +62,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public abstract class DrawableCatchHitObject : DrawableHitObject
{
- public virtual bool CanBePlated => false;
-
- public virtual bool StaysOnPlate => CanBePlated;
+ public virtual bool StaysOnPlate => HitObject.CanBePlated;
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
@@ -90,7 +85,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
if (CheckPosition == null) return;
if (timeOffset >= 0 && Result != null)
- ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss);
+ ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
protected override void UpdateStateTransforms(ArmedState state)
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
index cad8892283..688240fd86 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
@@ -4,12 +4,11 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Utils;
-using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
- public class DrawableDroplet : PalpableCatchHitObject
+ public class DrawableDroplet : PalpableDrawableCatchHitObject
{
public override bool StaysOnPlate => false;
@@ -21,11 +20,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
- ScaleContainer.Child = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new Pulp
- {
- Size = Size / 4,
- AccentColour = { BindTarget = AccentColour }
- });
+ ScaleContainer.Child = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new DropletPiece());
}
protected override void UpdateInitialTransforms()
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
index fae5a10d04..c1c34e4157 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
@@ -8,7 +8,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
- public class DrawableFruit : PalpableCatchHitObject
+ public class DrawableFruit : PalpableDrawableCatchHitObject
{
public DrawableFruit(Fruit h)
: base(h)
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs
new file mode 100644
index 0000000000..c2499446fa
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.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 osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables
+{
+ public class DropletPiece : CompositeDrawable
+ {
+ public DropletPiece()
+ {
+ Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableObject)
+ {
+ DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject;
+ var hitObject = drawableCatchObject.HitObject;
+
+ InternalChild = new Pulp
+ {
+ RelativeSizeAxes = Axes.Both,
+ AccentColour = { BindTarget = drawableObject.AccentColour }
+ };
+
+ if (hitObject.HyperDash)
+ {
+ AddInternal(new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(2f),
+ Depth = 1,
+ Children = new Drawable[]
+ {
+ new Circle
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
+ BorderThickness = 6,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ AlwaysPresent = true,
+ Alpha = 0.3f,
+ Blending = BlendingParameters.Additive,
+ RelativeSizeAxes = Axes.Both,
+ Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
+ }
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
index 7ac9f11ad6..4bffdab3d8 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
@@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -21,11 +20,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public const float RADIUS_ADJUST = 1.1f;
private Circle border;
-
private CatchHitObject hitObject;
- private readonly IBindable accentColour = new Bindable();
-
public FruitPiece()
{
RelativeSizeAxes = Axes.Both;
@@ -37,8 +33,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject;
hitObject = drawableCatchObject.HitObject;
- accentColour.BindTo(drawableCatchObject.AccentColour);
-
AddRangeInternal(new[]
{
getFruitFor(drawableCatchObject.HitObject.VisualRepresentation),
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs
index 1e7506a257..d3e4945611 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
- Radius = Size.X / 2,
+ Radius = DrawWidth / 2,
Colour = colour.NewValue.Darken(0.2f).Opacity(0.75f)
};
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Droplet.cs b/osu.Game.Rulesets.Catch/Objects/Droplet.cs
index 7b0bb3f0ae..9c1004a04b 100644
--- a/osu.Game.Rulesets.Catch/Objects/Droplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Droplet.cs
@@ -6,7 +6,7 @@ using osu.Game.Rulesets.Judgements;
namespace osu.Game.Rulesets.Catch.Objects
{
- public class Droplet : CatchHitObject
+ public class Droplet : PalpableCatchHitObject
{
public override Judgement CreateJudgement() => new CatchDropletJudgement();
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Fruit.cs b/osu.Game.Rulesets.Catch/Objects/Fruit.cs
index 6f0423b420..43486796ad 100644
--- a/osu.Game.Rulesets.Catch/Objects/Fruit.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Fruit.cs
@@ -6,7 +6,7 @@ using osu.Game.Rulesets.Judgements;
namespace osu.Game.Rulesets.Catch.Objects
{
- public class Fruit : CatchHitObject
+ public class Fruit : PalpableCatchHitObject
{
public override Judgement CreateJudgement() => new CatchJudgement();
}
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
index 5d11c574b1..a4f54bfe82 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
@@ -31,6 +31,9 @@ namespace osu.Game.Rulesets.Catch.Replays
public override Replay Generate()
{
+ if (Beatmap.HitObjects.Count == 0)
+ return Replay;
+
// todo: add support for HT DT
const double dash_speed = Catcher.BASE_SPEED;
const double movement_speed = dash_speed / 2;
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json
new file mode 100644
index 0000000000..3bde97070c
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json
@@ -0,0 +1,17 @@
+{
+ "Mappings": [{
+ "StartTime": 3368,
+ "Objects": [{
+ "StartTime": 3368,
+ "Position": 374
+ }]
+ },
+ {
+ "StartTime": 3501,
+ "Objects": [{
+ "StartTime": 3501,
+ "Position": 446
+ }]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset.osu b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset.osu
new file mode 100644
index 0000000000..6630f369d5
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset.osu
@@ -0,0 +1,20 @@
+osu file format v14
+
+[General]
+StackLeniency: 0.7
+Mode: 2
+
+[Difficulty]
+HPDrainRate:6
+CircleSize:4
+OverallDifficulty:9.6
+ApproachRate:9.6
+SliderMultiplier:1.9
+SliderTickRate:1
+
+[TimingPoints]
+2169,266.666666666667,4,2,1,70,1,0
+
+[HitObjects]
+374,60,3368,1,0,0:0:0:0:
+410,146,3501,1,2,0:1:0:0:
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs
index ff793a372e..0a444d923e 100644
--- a/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs
+++ b/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Catch.Scoring
{
switch (result)
{
- case HitResult.Perfect:
+ case HitResult.Great:
case HitResult.Miss:
return true;
}
diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
index 4c7bc4ab73..2cc05826b4 100644
--- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Scoring
{
public class CatchScoreProcessor : ScoreProcessor
{
- public override HitWindows CreateHitWindows() => new CatchHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
index d929da1a29..47224bd195 100644
--- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
@@ -6,6 +6,8 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Skinning;
using osuTK;
+using osuTK.Graphics;
+using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Catch.Skinning
{
@@ -51,6 +53,15 @@ namespace osu.Game.Rulesets.Catch.Skinning
case CatchSkinComponents.CatcherKiai:
return this.GetAnimation("fruit-catcher-kiai", true, true, true) ??
this.GetAnimation("fruit-ryuuta", true, true, true);
+
+ case CatchSkinComponents.CatchComboCounter:
+ var comboFont = GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score";
+
+ // For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
+ if (this.HasFont(comboFont))
+ return new LegacyComboCounter(Source);
+
+ break;
}
return null;
@@ -61,7 +72,12 @@ namespace osu.Game.Rulesets.Catch.Skinning
switch (lookup)
{
case CatchSkinColour colour:
- return Source.GetConfig(new SkinCustomColourLookup(colour));
+ var result = (Bindable)Source.GetConfig(new SkinCustomColourLookup(colour));
+ if (result == null)
+ return null;
+
+ result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value);
+ return (IBindable)result;
}
return Source.GetConfig(lookup);
diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs
new file mode 100644
index 0000000000..c8abc9e832
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs
@@ -0,0 +1,103 @@
+// 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.UI;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+using static osu.Game.Skinning.LegacySkinConfiguration;
+
+namespace osu.Game.Rulesets.Catch.Skinning
+{
+ ///
+ /// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
+ ///
+ public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter
+ {
+ private readonly LegacyRollingCounter counter;
+
+ private readonly LegacyRollingCounter explosion;
+
+ public LegacyComboCounter(ISkin skin)
+ {
+ var fontName = skin.GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score";
+ var fontOverlap = skin.GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f;
+
+ AutoSizeAxes = Axes.Both;
+
+ Alpha = 0f;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ Scale = new Vector2(0.8f);
+
+ InternalChildren = new Drawable[]
+ {
+ explosion = new LegacyRollingCounter(skin, fontName, fontOverlap)
+ {
+ Alpha = 0.65f,
+ Blending = BlendingParameters.Additive,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(1.5f),
+ },
+ counter = new LegacyRollingCounter(skin, fontName, fontOverlap)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ };
+ }
+
+ private int lastDisplayedCombo;
+
+ public void UpdateCombo(int combo, Color4? hitObjectColour = null)
+ {
+ if (combo == lastDisplayedCombo)
+ return;
+
+ // There may still be existing transforms to the counter (including value change after 250ms),
+ // finish them immediately before new transforms.
+ counter.SetCountWithoutRolling(lastDisplayedCombo);
+
+ lastDisplayedCombo = combo;
+
+ if (Time.Elapsed < 0)
+ {
+ // needs more work to make rewind somehow look good.
+ // basically we want the previous increment to play... or turning off RemoveCompletedTransforms (not feasible from a performance angle).
+ Hide();
+ return;
+ }
+
+ // Combo fell to zero, roll down and fade out the counter.
+ if (combo == 0)
+ {
+ counter.Current.Value = 0;
+ explosion.Current.Value = 0;
+
+ this.FadeOut(400, Easing.Out);
+ }
+ else
+ {
+ this.FadeInFromZero().Then().Delay(1000).FadeOut(300);
+
+ counter.ScaleTo(1.5f)
+ .ScaleTo(0.8f, 250, Easing.Out)
+ .OnComplete(c => c.SetCountWithoutRolling(combo));
+
+ counter.Delay(250)
+ .ScaleTo(1f)
+ .ScaleTo(1.1f, 60).Then().ScaleTo(1f, 30);
+
+ explosion.Colour = hitObjectColour ?? Color4.White;
+
+ explosion.SetCountWithoutRolling(combo);
+ explosion.ScaleTo(1.5f)
+ .ScaleTo(1.9f, 400, Easing.Out)
+ .FadeOutFromOne(400);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
index 5be54d3882..381d066750 100644
--- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
@@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Catch.Skinning
colouredSprite = new Sprite
{
Texture = skin.GetTexture(lookupName),
- Colour = drawableObject.AccentColour.Value,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
@@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Skinning
{
base.LoadComplete();
- accentColour.BindValueChanged(colour => colouredSprite.Colour = colour.NewValue, true);
+ accentColour.BindValueChanged(colour => colouredSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs
new file mode 100644
index 0000000000..75feb21298
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs
@@ -0,0 +1,62 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using JetBrains.Annotations;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ ///
+ /// Represents a component that displays a skinned and handles combo judgement results for updating it accordingly.
+ ///
+ public class CatchComboDisplay : SkinnableDrawable
+ {
+ private int currentCombo;
+
+ [CanBeNull]
+ public ICatchComboCounter ComboCounter => Drawable as ICatchComboCounter;
+
+ public CatchComboDisplay()
+ : base(new CatchSkinComponent(CatchSkinComponents.CatchComboCounter), _ => Empty())
+ {
+ }
+
+ protected override void SkinChanged(ISkinSource skin, bool allowFallback)
+ {
+ base.SkinChanged(skin, allowFallback);
+ ComboCounter?.UpdateCombo(currentCombo);
+ }
+
+ public void OnNewResult(DrawableCatchHitObject judgedObject, JudgementResult result)
+ {
+ if (!result.Type.AffectsCombo() || !result.HasResult)
+ return;
+
+ if (!result.IsHit)
+ {
+ updateCombo(0, null);
+ return;
+ }
+
+ updateCombo(result.ComboAtJudgement + 1, judgedObject.AccentColour.Value);
+ }
+
+ public void OnRevertResult(DrawableCatchHitObject judgedObject, JudgementResult result)
+ {
+ if (!result.Type.AffectsCombo() || !result.HasResult)
+ return;
+
+ updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value);
+ }
+
+ private void updateCombo(int newCombo, Color4? hitObjectColour)
+ {
+ currentCombo = newCombo;
+ ComboCounter?.UpdateCombo(newCombo, hitObjectColour);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index 154e1576db..735d7fc300 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.UI
explodingFruitContainer,
CatcherArea.MovableCatcher.CreateProxiedContent(),
HitObjectContainer,
- CatcherArea
+ CatcherArea,
};
}
@@ -62,6 +62,7 @@ namespace osu.Game.Rulesets.Catch.UI
public override void Add(DrawableHitObject h)
{
h.OnNewResult += onNewResult;
+ h.OnRevertResult += onRevertResult;
base.Add(h);
@@ -70,6 +71,9 @@ namespace osu.Game.Rulesets.Catch.UI
}
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
- => CatcherArea.OnResult((DrawableCatchHitObject)judgedObject, result);
+ => CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);
+
+ private void onRevertResult(DrawableHitObject judgedObject, JudgementResult result)
+ => CatcherArea.OnRevertResult((DrawableCatchHitObject)judgedObject, result);
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
index 8ee23461ba..efc1b24ed5 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
@@ -10,15 +10,21 @@ namespace osu.Game.Rulesets.Catch.UI
{
public class CatchPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{
+ private const float playfield_size_adjust = 0.8f;
+
protected override Container Content => content;
private readonly Container content;
public CatchPlayfieldAdjustmentContainer()
{
- Anchor = Anchor.TopCentre;
- Origin = Anchor.TopCentre;
+ // because we are using centre anchor/origin, we will need to limit visibility in the future
+ // to ensure tall windows do not get a readability advantage.
+ // it may be possible to bake the catch-specific offsets (-100..340 mentioned below) into new values
+ // which are compatible with TopCentre alignment.
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
- Size = new Vector2(0.86f); // matches stable's vertical offset for catcher plate
+ Size = new Vector2(playfield_size_adjust);
InternalChild = new Container
{
@@ -27,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.UI
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
FillAspectRatio = 4f / 3,
- Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both }
+ Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both, }
};
}
@@ -40,8 +46,14 @@ namespace osu.Game.Rulesets.Catch.UI
{
base.Update();
+ // in stable, fruit fall vertically from -100 to 340.
+ // to emulate this, we want to make our playfield 440 gameplay pixels high.
+ // we then offset it -100 vertically in the position set below.
+ const float stable_v_offset_ratio = 440 / 384f;
+
Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.WIDTH);
- Size = Vector2.Divide(Vector2.One, Scale);
+ Position = new Vector2(0, -100 * stable_v_offset_ratio + Scale.X);
+ Size = Vector2.Divide(new Vector2(1, stable_v_offset_ratio), Scale);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 8820dff730..9289a6162c 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Catch.UI
///
/// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable.
///
- private const float allowed_catch_range = 0.8f;
+ public const float ALLOWED_CATCH_RANGE = 0.8f;
///
/// The drawable catcher for .
@@ -166,7 +166,7 @@ namespace osu.Game.Rulesets.Catch.UI
///
/// The scale of the catcher.
internal static float CalculateCatchWidth(Vector2 scale)
- => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * allowed_catch_range;
+ => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE;
///
/// Calculates the width of the area used for attempting catches in gameplay.
@@ -216,6 +216,9 @@ namespace osu.Game.Rulesets.Catch.UI
/// Whether the catch is possible.
public bool AttemptCatch(CatchHitObject fruit)
{
+ if (!fruit.CanBePlated)
+ return false;
+
var halfCatchWidth = catchWidth * 0.5f;
// this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
@@ -226,9 +229,8 @@ namespace osu.Game.Rulesets.Catch.UI
catchObjectPosition >= catcherPosition - halfCatchWidth &&
catchObjectPosition <= catcherPosition + halfCatchWidth;
- // only update hyperdash state if we are catching a fruit.
- // exceptions are Droplets and JuiceStreams.
- if (!(fruit is Fruit)) return validCatch;
+ // only update hyperdash state if we are not catching a tiny droplet.
+ if (fruit is TinyDroplet) return validCatch;
if (validCatch && fruit.HyperDash)
{
@@ -283,8 +285,6 @@ namespace osu.Game.Rulesets.Catch.UI
private void runHyperDashStateTransition(bool hyperDashing)
{
- trails.HyperDashTrailsColour = hyperDashColour;
- trails.EndGlowSpritesColour = hyperDashEndGlowColour;
updateTrailVisibility();
if (hyperDashing)
@@ -401,6 +401,9 @@ namespace osu.Game.Rulesets.Catch.UI
skin.GetConfig(CatchSkinColour.HyperDashAfterImage)?.Value ??
hyperDashColour;
+ trails.HyperDashTrailsColour = hyperDashColour;
+ trails.EndGlowSpritesColour = hyperDashEndGlowColour;
+
runHyperDashStateTransition(HyperDashing);
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index 4255c3b1af..5e794a76aa 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osuTK;
@@ -23,6 +24,7 @@ namespace osu.Game.Rulesets.Catch.UI
public Func> CreateDrawableRepresentation;
public readonly Catcher MovableCatcher;
+ private readonly CatchComboDisplay comboDisplay;
public Container ExplodingFruitTarget
{
@@ -34,12 +36,24 @@ namespace osu.Game.Rulesets.Catch.UI
public CatcherArea(BeatmapDifficulty difficulty = null)
{
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
- Child = MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X };
+ Children = new Drawable[]
+ {
+ comboDisplay = new CatchComboDisplay
+ {
+ RelativeSizeAxes = Axes.None,
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.Centre,
+ Margin = new MarginPadding { Bottom = 350f },
+ X = CatchPlayfield.CENTER_X
+ },
+ MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X },
+ };
}
- public void OnResult(DrawableCatchHitObject fruit, JudgementResult result)
+ public void OnNewResult(DrawableCatchHitObject fruit, JudgementResult result)
{
- if (result.Judgement is IgnoreJudgement)
+ if (!result.Type.IsScorable())
return;
void runAfterLoaded(Action action)
@@ -55,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.UI
lastPlateableFruit.OnLoadComplete += _ => action();
}
- if (result.IsHit && fruit.CanBePlated)
+ if (result.IsHit && fruit.HitObject.CanBePlated)
{
// create a new (cloned) fruit to stay on the plate. the original is faded out immediately.
var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit.HitObject);
@@ -86,8 +100,13 @@ namespace osu.Game.Rulesets.Catch.UI
else
MovableCatcher.Drop();
}
+
+ comboDisplay.OnNewResult(fruit, result);
}
+ public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result)
+ => comboDisplay.OnRevertResult(fruit, result);
+
public void OnReleased(CatchAction action)
{
}
@@ -105,6 +124,8 @@ namespace osu.Game.Rulesets.Catch.UI
if (state?.CatcherX != null)
MovableCatcher.X = state.CatcherX.Value;
+
+ comboDisplay.X = MovableCatcher.X;
}
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
index bab3cb748b..f7e9fd19a7 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.UI
private readonly Container hyperDashTrails;
private readonly Container endGlowSprites;
- private Color4 hyperDashTrailsColour;
+ private Color4 hyperDashTrailsColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
public Color4 HyperDashTrailsColour
{
@@ -35,11 +35,11 @@ namespace osu.Game.Rulesets.Catch.UI
return;
hyperDashTrailsColour = value;
- hyperDashTrails.FadeColour(hyperDashTrailsColour, Catcher.HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
+ hyperDashTrails.Colour = hyperDashTrailsColour;
}
}
- private Color4 endGlowSpritesColour;
+ private Color4 endGlowSpritesColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
public Color4 EndGlowSpritesColour
{
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.UI
return;
endGlowSpritesColour = value;
- endGlowSprites.FadeColour(endGlowSpritesColour, Catcher.HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
+ endGlowSprites.Colour = endGlowSpritesColour;
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs b/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs
new file mode 100644
index 0000000000..cfb6879067
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.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 osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ ///
+ /// An interface providing a set of methods to update the combo counter.
+ ///
+ public interface ICatchComboCounter : IDrawable
+ {
+ ///
+ /// Updates the counter to animate a transition from the old combo value it had to the current provided one.
+ ///
+ ///
+ /// This is called regardless of whether the clock is rewinding.
+ ///
+ /// The new combo value.
+ /// The colour of the object if hit, null on miss.
+ void UpdateCombo(int combo, Color4? hitObjectColour = null);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs
similarity index 97%
rename from osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
rename to osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs
index 0fe4a3c669..ece523e84c 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs
@@ -16,7 +16,7 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK.Graphics;
-namespace osu.Game.Rulesets.Mania.Tests
+namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public abstract class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene
{
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs
similarity index 95%
rename from osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs
rename to osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs
index 149f6582ab..176fbba921 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs
@@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
using osuTK.Graphics;
-namespace osu.Game.Rulesets.Mania.Tests
+namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene
{
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs
similarity index 96%
rename from osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs
rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs
index 3b9c03b86a..d3afbc63eb 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs
@@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
-namespace osu.Game.Rulesets.Mania.Tests
+namespace osu.Game.Rulesets.Mania.Tests.Editor
{
[TestFixture]
public class TestSceneEditor : EditorTestScene
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs
similarity index 93%
rename from osu.Game.Rulesets.Mania.Tests/TestSceneHoldNotePlacementBlueprint.cs
rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs
index b4332264b9..87c74a12cf 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs
@@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-namespace osu.Game.Rulesets.Mania.Tests
+namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
{
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs
similarity index 97%
rename from osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs
rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs
index 90394f3d1b..24f4c6858e 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs
@@ -12,7 +12,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
-namespace osu.Game.Rulesets.Mania.Tests
+namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneHoldNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
{
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
similarity index 98%
rename from osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs
rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
index 639be0bc11..654b752001 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
@@ -20,7 +20,7 @@ using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
using osuTK;
-namespace osu.Game.Rulesets.Mania.Tests
+namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneManiaBeatSnapGrid : EditorClockTestScene
{
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
similarity index 99%
rename from osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs
rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
index 1a3fa29d4a..c9551ee79e 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
@@ -23,7 +23,7 @@ using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
-namespace osu.Game.Rulesets.Mania.Tests
+namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneManiaHitObjectComposer : EditorClockTestScene
{
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs
similarity index 97%
rename from osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs
rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs
index 2d97e61aa5..36c34a8fb9 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs
@@ -18,7 +18,7 @@ using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
-namespace osu.Game.Rulesets.Mania.Tests
+namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
{
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs
similarity index 96%
rename from osu.Game.Rulesets.Mania.Tests/TestSceneNoteSelectionBlueprint.cs
rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs
index 1514bdf0bd..0e47a12a8e 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs
@@ -12,7 +12,7 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK;
-namespace osu.Game.Rulesets.Mania.Tests
+namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
{
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
index d0ff1fab43..0c57267970 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
@@ -14,11 +14,13 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests
{
[TestFixture]
+ [Timeout(10000)]
public class ManiaBeatmapConversionTest : BeatmapConversionTest
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase("basic")]
+ [TestCase("zero-length-slider")]
public void Test(string name) => base.Test(name);
protected override IEnumerable CreateConvertValue(HitObject hitObject)
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs
index 957743c5f1..b22687a0a7 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs
@@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime), typeof(ManiaModPerfect) })]
[TestCase(LegacyMods.Random | LegacyMods.SuddenDeath, new[] { typeof(ManiaModRandom), typeof(ManiaModSuddenDeath) })]
+ [TestCase(LegacyMods.Flashlight | LegacyMods.Mirror, new[] { typeof(ManiaModFlashlight), typeof(ManiaModMirror) })]
public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods);
protected override Ruleset CreateRuleset() => new ManiaRuleset();
diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs
new file mode 100644
index 0000000000..f2cc254e38
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.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 NUnit.Framework;
+using osu.Game.Rulesets.Mania.Mods;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests.Mods
+{
+ public class TestSceneManiaModInvert : ModTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
+
+ [Test]
+ public void TestInversion() => CreateModTest(new ModTestData
+ {
+ Mod = new ManiaModInvert(),
+ PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs
index ff4865c71d..8ba58e3af3 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs
@@ -22,18 +22,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[Cached]
private readonly Column column;
- public ColumnTestContainer(int column, ManiaAction action)
+ public ColumnTestContainer(int column, ManiaAction action, bool showColumn = false)
{
- this.column = new Column(column)
+ InternalChildren = new[]
{
- Action = { Value = action },
- AccentColour = Color4.Orange,
- ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd
- };
-
- InternalChild = content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
- {
- RelativeSizeAxes = Axes.Both
+ this.column = new Column(column)
+ {
+ Action = { Value = action },
+ AccentColour = Color4.Orange,
+ ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd,
+ Alpha = showColumn ? 1 : 0
+ },
+ content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ this.column.TopLevelContainer.CreateProxy()
};
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
index 18eebada00..d24c81dac6 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
- new ColumnTestContainer(0, ManiaAction.Key1)
+ new ColumnTestContainer(0, ManiaAction.Key1, true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
}));
})
},
- new ColumnTestContainer(1, ManiaAction.Key2)
+ new ColumnTestContainer(1, ManiaAction.Key2, true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs
index 95e86de884..9c4c2b3d5b 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
+using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@@ -13,7 +14,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
public class TestSceneHoldNote : ManiaHitObjectTestScene
{
- public TestSceneHoldNote()
+ [Test]
+ public void TestHoldNote()
{
AddToggleStep("toggle hitting", v =>
{
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
index 87c84cf89c..a15fb392d6 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Skinning;
@@ -13,7 +14,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground())
+ SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: new StageDefinition { Columns = 4 }),
+ _ => new DefaultStageBackground())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
index 4e99068ed5..bceee1c599 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
@@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => 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/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 95072cf4f8..5cb1519196 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -45,9 +45,9 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
- assertNoteJudgement(HitResult.Perfect);
+ assertNoteJudgement(HitResult.IgnoreHit);
}
///
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
}
@@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
}
@@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
- assertTickJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Miss);
}
@@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
- assertTickJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Perfect);
}
@@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
- assertTickJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
}
@@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
- assertTickJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Miss);
}
@@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
- assertTickJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Meh);
}
@@ -199,7 +199,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Miss);
}
@@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.Perfect);
+ assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Meh);
}
@@ -235,7 +235,7 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
- assertTickJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Meh);
}
@@ -280,10 +280,10 @@ namespace osu.Game.Rulesets.Mania.Tests
}, beatmap);
AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
- .All(j => j.Type == HitResult.Miss));
+ .All(j => !j.Type.IsHit()));
AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
- .All(j => j.Type == HitResult.Perfect));
+ .All(j => j.Type.IsHit()));
}
private void assertHeadJudgement(HitResult result)
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
index dd5fd93710..6b8f5d5d9d 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
@@ -28,25 +28,33 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestFixture]
public class TestSceneNotes : OsuTestScene
{
- [BackgroundDependencyLoader]
- private void load()
+ [Test]
+ public void TestVariousNotes()
{
- Child = new FillFlowContainer
+ DrawableNote note1 = null;
+ DrawableNote note2 = null;
+ DrawableHoldNote holdNote1 = null;
+ DrawableHoldNote holdNote2 = null;
+
+ AddStep("create notes", () =>
{
- Clock = new FramedClock(new ManualClock()),
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
- Spacing = new Vector2(20),
- Children = new[]
+ Child = new FillFlowContainer
{
- createNoteDisplay(ScrollingDirection.Down, 1, out var note1),
- createNoteDisplay(ScrollingDirection.Up, 2, out var note2),
- createHoldNoteDisplay(ScrollingDirection.Down, 1, out var holdNote1),
- createHoldNoteDisplay(ScrollingDirection.Up, 2, out var holdNote2),
- }
- };
+ Clock = new FramedClock(new ManualClock()),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(20),
+ Children = new[]
+ {
+ createNoteDisplay(ScrollingDirection.Down, 1, out note1),
+ createNoteDisplay(ScrollingDirection.Up, 2, out note2),
+ createHoldNoteDisplay(ScrollingDirection.Down, 1, out holdNote1),
+ createHoldNoteDisplay(ScrollingDirection.Up, 2, out holdNote2),
+ }
+ };
+ });
AddAssert("note 1 facing downwards", () => verifyAnchors(note1, Anchor.y2));
AddAssert("note 2 facing upwards", () => verifyAnchors(note2, Anchor.y0));
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
new file mode 100644
index 0000000000..ab840e1c46
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
@@ -0,0 +1,117 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Extensions.TypeExtensions;
+using osu.Framework.Screens;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Replays;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestSceneOutOfOrderHits : RateAdjustedBeatmapTestScene
+ {
+ [Test]
+ public void TestPreviousHitWindowDoesNotExtendPastNextObject()
+ {
+ var objects = new List();
+ var frames = new List();
+
+ for (int i = 0; i < 7; i++)
+ {
+ double time = 1000 + i * 100;
+
+ objects.Add(new Note { StartTime = time });
+
+ if (i > 0)
+ {
+ frames.Add(new ManiaReplayFrame(time + 10, ManiaAction.Key1));
+ frames.Add(new ManiaReplayFrame(time + 11));
+ }
+ }
+
+ performTest(objects, frames);
+
+ addJudgementAssert(objects[0], HitResult.Miss);
+
+ for (int i = 1; i < 7; i++)
+ {
+ addJudgementAssert(objects[i], HitResult.Perfect);
+ addJudgementOffsetAssert(objects[i], 10);
+ }
+ }
+
+ private void addJudgementAssert(ManiaHitObject hitObject, HitResult result)
+ {
+ AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
+ () => judgementResults.Single(r => r.HitObject == hitObject).Type == result);
+ }
+
+ private void addJudgementOffsetAssert(ManiaHitObject hitObject, double offset)
+ {
+ AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
+ () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
+ }
+
+ private ScoreAccessibleReplayPlayer currentPlayer;
+ private List judgementResults;
+
+ private void performTest(List hitObjects, List frames)
+ {
+ AddStep("load player", () =>
+ {
+ Beatmap.Value = CreateWorkingBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })
+ {
+ HitObjects = hitObjects,
+ BeatmapInfo =
+ {
+ Ruleset = new ManiaRuleset().RulesetInfo
+ },
+ });
+
+ Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+
+ var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
+
+ p.OnLoadComplete += _ =>
+ {
+ p.ScoreProcessor.NewJudgement += result =>
+ {
+ if (currentPlayer == p) judgementResults.Add(result);
+ };
+ };
+
+ LoadScreen(currentPlayer = p);
+ judgementResults = new List();
+ });
+
+ AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
+ AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
+ AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
+ }
+
+ private class ScoreAccessibleReplayPlayer : ReplayPlayer
+ {
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+
+ protected override bool PauseOnFocusLost => false;
+
+ public ScoreAccessibleReplayPlayer(Score score)
+ : base(score, false, false)
+ {
+ }
+ }
+ }
+}
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 972cbec4a2..892f27d27f 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
index dc24a344e9..d1d5adea75 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
@@ -41,14 +40,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
new BeatmapStatistic
{
Name = @"Note Count",
+ CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
Content = notes.ToString(),
- Icon = FontAwesome.Regular.Circle
},
new BeatmapStatistic
{
Name = @"Hold Note Count",
+ CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
Content = holdnotes.ToString(),
- Icon = FontAwesome.Regular.Circle
},
};
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index b025ac7992..524ea27efa 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -5,7 +5,7 @@ using osu.Game.Rulesets.Mania.Objects;
using System;
using System.Linq;
using System.Collections.Generic;
-using osu.Framework.Utils;
+using System.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
@@ -69,14 +69,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
- protected override Beatmap ConvertBeatmap(IBeatmap original)
+ protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty;
int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
Random = new FastRandom(seed);
- return base.ConvertBeatmap(original);
+ return base.ConvertBeatmap(original, cancellationToken);
}
protected override Beatmap CreateBeatmap()
@@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
return beatmap;
}
- protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap)
+ protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
{
if (original is ManiaHitObject maniaOriginal)
{
@@ -167,8 +167,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
var positionData = original as IHasPosition;
- for (double time = original.StartTime; !Precision.DefinitelyBigger(time, generator.EndTime); time += generator.SegmentDuration)
+ for (int i = 0; i <= generator.SpanCount; i++)
{
+ double time = original.StartTime + generator.SegmentDuration * i;
+
recordNote(time, positionData?.Position ?? Vector2.Zero);
computeDensity(time);
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index d03eb0b3c9..fe146c5324 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -27,8 +27,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
public readonly double EndTime;
public readonly double SegmentDuration;
-
- private readonly int spanCount;
+ public readonly int SpanCount;
private PatternType convertType;
@@ -42,20 +41,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var distanceData = hitObject as IHasDistance;
var repeatsData = hitObject as IHasRepeats;
- spanCount = repeatsData?.SpanCount() ?? 1;
+ SpanCount = repeatsData?.SpanCount() ?? 1;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
// The true distance, accounting for any repeats
- double distance = (distanceData?.Distance ?? 0) * spanCount;
+ double distance = (distanceData?.Distance ?? 0) * SpanCount;
// The velocity of the osu! hit object - calculated as the velocity of a slider
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength;
// The duration of the osu! hit object
double osuDuration = distance / osuVelocity;
EndTime = hitObject.StartTime + osuDuration;
- SegmentDuration = (EndTime - HitObject.StartTime) / spanCount;
+ SegmentDuration = (EndTime - HitObject.StartTime) / SpanCount;
}
public override IEnumerable Generate()
@@ -96,7 +95,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern;
}
- if (spanCount > 1)
+ if (SpanCount > 1)
{
if (SegmentDuration <= 90)
return generateRandomHoldNotes(HitObject.StartTime, 1);
@@ -104,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (SegmentDuration <= 120)
{
convertType |= PatternType.ForceNotStack;
- return generateRandomNotes(HitObject.StartTime, spanCount + 1);
+ return generateRandomNotes(HitObject.StartTime, SpanCount + 1);
}
if (SegmentDuration <= 160)
@@ -117,7 +116,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (duration >= 4000)
return generateNRandomNotes(HitObject.StartTime, 0.23, 0, 0);
- if (SegmentDuration > 400 && spanCount < TotalColumns - 1 - RandomStart)
+ if (SegmentDuration > 400 && SpanCount < TotalColumns - 1 - RandomStart)
return generateTiledHoldNotes(HitObject.StartTime);
return generateHoldAndNormalNotes(HitObject.StartTime);
@@ -251,7 +250,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int column = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
bool increasing = Random.NextDouble() > 0.5;
- for (int i = 0; i <= spanCount; i++)
+ for (int i = 0; i <= SpanCount; i++)
{
addToPattern(pattern, column, startTime, startTime);
startTime += SegmentDuration;
@@ -302,7 +301,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
- for (int i = 0; i <= spanCount; i++)
+ for (int i = 0; i <= SpanCount; i++)
{
addToPattern(pattern, nextColumn, startTime, startTime);
@@ -393,7 +392,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern();
- int columnRepeat = Math.Min(spanCount, TotalColumns);
+ int columnRepeat = Math.Min(SpanCount, TotalColumns);
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
@@ -447,7 +446,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var rowPattern = new Pattern();
- for (int i = 0; i <= spanCount; i++)
+ for (int i = 0; i <= SpanCount; i++)
{
if (!(ignoreHead && startTime == HitObject.StartTime))
{
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs
index 2557f2acdf..3052fc7d34 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs
@@ -21,14 +21,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
///
/// The 0-based column index.
/// Whether the column is a special column.
- public bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2;
+ public readonly bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2;
///
/// Get the type of column given a column index.
///
/// The 0-based column index.
/// The type of the column.
- public ColumnType GetTypeOfColumn(int column)
+ public readonly ColumnType GetTypeOfColumn(int column)
{
if (IsSpecialColumn(column))
return ColumnType.Special;
diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
index 7e84f17809..756f2b7b2f 100644
--- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
+++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
{
base.InitialiseDefaults();
- Set(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 1);
+ Set(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5);
Set(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
index 37cba1fd3c..b08c520c54 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mania.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Mods;
+using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@@ -43,6 +44,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
Mods = mods,
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate,
+ MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
Skills = skills
};
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
index 91383c5548..00bec18a45 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
-using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@@ -29,8 +28,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private int countMeh;
private int countMiss;
- public ManiaPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score)
- : base(ruleset, beatmap, score)
+ public ManiaPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
+ : base(ruleset, attributes, score)
{
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs
index efcfe11dad..5fa687298a 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
@@ -15,7 +16,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
AccentColour.Value = colours.Yellow;
Background.Alpha = 0.5f;
- Foreground.Alpha = 0;
}
+
+ protected override Drawable CreateForeground() => base.CreateForeground().With(d => d.Alpha = 0);
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs b/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs
index 295bf417c4..a5f10ed436 100644
--- a/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs
+++ b/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
@@ -14,6 +16,8 @@ namespace osu.Game.Rulesets.Mania.Edit
{
}
+ public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
+
public override PlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint();
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs b/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs
index 50b5f9a8fe..9f54152596 100644
--- a/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs
+++ b/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
@@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Mania.Edit
{
}
+ public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
+
public override PlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint();
}
}
diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
index 294aab1e4e..ee6cbbc828 100644
--- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
+++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs
@@ -7,18 +7,6 @@ namespace osu.Game.Rulesets.Mania.Judgements
{
public class HoldNoteTickJudgement : ManiaJudgement
{
- protected override int NumericResultFor(HitResult result) => 20;
-
- protected override double HealthIncreaseFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Perfect:
- return 0.01;
- }
- }
+ public override HitResult MaxResult => HitResult.LargeTickHit;
}
}
diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
index 53967ffa05..d28b7bdf58 100644
--- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
@@ -8,27 +8,33 @@ namespace osu.Game.Rulesets.Mania.Judgements
{
public class ManiaJudgement : Judgement
{
- protected override int NumericResultFor(HitResult result)
+ protected override double HealthIncreaseFor(HitResult result)
{
switch (result)
{
- default:
- return 0;
+ case HitResult.LargeTickHit:
+ return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
+
+ case HitResult.LargeTickMiss:
+ return -DEFAULT_MAX_HEALTH_INCREASE * 0.1;
case HitResult.Meh:
- return 50;
+ return -DEFAULT_MAX_HEALTH_INCREASE * 0.5;
case HitResult.Ok:
- return 100;
+ return -DEFAULT_MAX_HEALTH_INCREASE * 0.3;
case HitResult.Good:
- return 200;
+ return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
case HitResult.Great:
- return 300;
+ return DEFAULT_MAX_HEALTH_INCREASE * 0.8;
case HitResult.Perfect:
- return 350;
+ return DEFAULT_MAX_HEALTH_INCREASE;
+
+ default:
+ return base.HealthIncreaseFor(result);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index 68dce8b139..ecb09ebe85 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -12,7 +12,6 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
-using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types;
@@ -35,7 +34,6 @@ using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Mania
{
- [ExcludeFromDynamicCompile]
public class ManiaRuleset : Ruleset, ILegacyRuleset
{
///
@@ -51,7 +49,7 @@ namespace osu.Game.Rulesets.Mania
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
- public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new ManiaPerformanceCalculator(this, beatmap, score);
+ public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new ManiaPerformanceCalculator(this, attributes, score);
public const string SHORT_NAME = "mania";
@@ -126,6 +124,9 @@ namespace osu.Game.Rulesets.Mania
if (mods.HasFlag(LegacyMods.Random))
yield return new ManiaModRandom();
+
+ if (mods.HasFlag(LegacyMods.Mirror))
+ yield return new ManiaModMirror();
}
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
@@ -175,6 +176,10 @@ namespace osu.Game.Rulesets.Mania
case ManiaModFadeIn _:
value |= LegacyMods.FadeIn;
break;
+
+ case ManiaModMirror _:
+ value |= LegacyMods.Mirror;
+ break;
}
}
@@ -220,6 +225,7 @@ namespace osu.Game.Rulesets.Mania
new ManiaModDualStages(),
new ManiaModMirror(),
new ManiaModDifficultyAdjust(),
+ new ManiaModInvert(),
};
case ModType.Automation:
@@ -325,6 +331,16 @@ namespace osu.Game.Rulesets.Mania
Height = 250
}),
}
+ },
+ new StatisticRow
+ {
+ Columns = new[]
+ {
+ new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
+ {
+ new UnstableRate(score.HitEvents)
+ }))
+ }
}
};
}
diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
index 2ebfd0cfc1..de77af8306 100644
--- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
@@ -29,12 +29,13 @@ namespace osu.Game.Rulesets.Mania
new SettingsEnumDropdown
{
LabelText = "Scrolling direction",
- Bindable = config.GetBindable(ManiaRulesetSetting.ScrollDirection)
+ Current = config.GetBindable(ManiaRulesetSetting.ScrollDirection)
},
new SettingsSlider
{
LabelText = "Scroll speed",
- Bindable = config.GetBindable(ManiaRulesetSetting.ScrollTime)
+ Current = config.GetBindable(ManiaRulesetSetting.ScrollTime),
+ KeyboardStep = 5
},
};
}
diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
index c0c8505f44..f078345fc1 100644
--- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Skinning;
@@ -14,15 +15,23 @@ namespace osu.Game.Rulesets.Mania
///
public readonly int? TargetColumn;
+ ///
+ /// The intended for this component.
+ /// May be null if the component is not a direct member of a .
+ ///
+ public readonly StageDefinition? StageDefinition;
+
///
/// Creates a new .
///
/// The component.
/// The intended index for this component. May be null if the component does not exist in a .
- public ManiaSkinComponent(ManiaSkinComponents component, int? targetColumn = null)
+ /// 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)
: base(component)
{
TargetColumn = targetColumn;
+ StageDefinition = stageDefinition;
}
protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME;
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs
new file mode 100644
index 0000000000..1ea45c295c
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs
@@ -0,0 +1,76 @@
+// 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.Graphics.Sprites;
+using osu.Game.Audio;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Rulesets.Mania.Mods
+{
+ public class ManiaModInvert : Mod, IApplicableAfterBeatmapConversion
+ {
+ public override string Name => "Invert";
+
+ public override string Acronym => "IN";
+ public override double ScoreMultiplier => 1;
+
+ public override string Description => "Hold the keys. To the beat.";
+
+ public override IconUsage? Icon => FontAwesome.Solid.YinYang;
+
+ public override ModType Type => ModType.Conversion;
+
+ public void ApplyToBeatmap(IBeatmap beatmap)
+ {
+ var maniaBeatmap = (ManiaBeatmap)beatmap;
+
+ var newObjects = new List();
+
+ foreach (var column in maniaBeatmap.HitObjects.GroupBy(h => h.Column))
+ {
+ var newColumnObjects = new List();
+
+ var locations = column.OfType().Select(n => (startTime: n.StartTime, samples: n.Samples))
+ .Concat(column.OfType().SelectMany(h => new[]
+ {
+ (startTime: h.StartTime, samples: h.GetNodeSamples(0)),
+ (startTime: h.EndTime, samples: h.GetNodeSamples(1))
+ }))
+ .OrderBy(h => h.startTime).ToList();
+
+ for (int i = 0; i < locations.Count - 1; i++)
+ {
+ // Full duration of the hold note.
+ double duration = locations[i + 1].startTime - locations[i].startTime;
+
+ // Beat length at the end of the hold note.
+ double beatLength = beatmap.ControlPointInfo.TimingPointAt(locations[i + 1].startTime).BeatLength;
+
+ // Decrease the duration by at most a 1/4 beat to ensure there's no instantaneous notes.
+ duration = Math.Max(duration / 2, duration - beatLength / 4);
+
+ newColumnObjects.Add(new HoldNote
+ {
+ Column = column.Key,
+ StartTime = locations[i].startTime,
+ Duration = duration,
+ NodeSamples = new List> { locations[i].samples, Array.Empty() }
+ });
+ }
+
+ newObjects.AddRange(newColumnObjects);
+ }
+
+ maniaBeatmap.HitObjects = newObjects.OrderBy(h => h.StartTime).ToList();
+
+ // No breaks
+ maniaBeatmap.Breaks.Clear();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 0c5289efe1..f6d539c91b 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
@@ -32,7 +33,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
private readonly Container tailContainer;
private readonly Container tickContainer;
- private readonly Drawable bodyPiece;
+ ///
+ /// 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;
+
+ ///
+ /// 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 readonly SkinnableDrawable bodyPiece;
///
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
@@ -44,24 +55,54 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
public bool HasBroken { get; private set; }
+ ///
+ /// Whether the hold note has been released potentially without having caused a break.
+ ///
+ private double? releaseTime;
+
public DrawableHoldNote(HoldNote hitObject)
: base(hitObject)
{
RelativeSizeAxes = Axes.X;
- AddRangeInternal(new[]
+ Container maskedContents;
+
+ AddRangeInternal(new Drawable[]
{
+ sizingContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ maskingContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = maskedContents = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ }
+ },
+ headContainer = new Container { RelativeSizeAxes = Axes.Both }
+ }
+ },
bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece
{
- RelativeSizeAxes = Axes.Both
+ RelativeSizeAxes = Axes.Both,
})
{
RelativeSizeAxes = Axes.X
},
tickContainer = new Container { RelativeSizeAxes = Axes.Both },
- headContainer = new Container { RelativeSizeAxes = Axes.Both },
tailContainer = new Container { RelativeSizeAxes = Axes.Both },
});
+
+ maskedContents.AddRange(new[]
+ {
+ bodyPiece.CreateProxy(),
+ tickContainer.CreateProxy(),
+ tailContainer.CreateProxy(),
+ });
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
@@ -127,7 +168,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
base.OnDirectionChanged(e);
- bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
+ if (e.NewValue == ScrollingDirection.Up)
+ {
+ bodyPiece.Anchor = bodyPiece.Origin = Anchor.TopLeft;
+ sizingContainer.Anchor = sizingContainer.Origin = Anchor.BottomLeft;
+ }
+ else
+ {
+ bodyPiece.Anchor = bodyPiece.Origin = Anchor.BottomLeft;
+ sizingContainer.Anchor = sizingContainer.Origin = Anchor.TopLeft;
+ }
}
public override void PlaySamples()
@@ -135,13 +185,48 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// Samples are played by the head/tail notes.
}
+ public override void OnKilled()
+ {
+ base.OnKilled();
+ (bodyPiece.Drawable as IHoldNoteBody)?.Recycle();
+ }
+
protected override void Update()
{
base.Update();
- // Make the body piece not lie under the head note
+ if (Time.Current < releaseTime)
+ releaseTime = null;
+
+ // Pad the full size container so its contents (i.e. the masking container) reach under the tail.
+ // This is required for the tail to not be masked away, since it lies outside the bounds of the hold note.
+ sizingContainer.Padding = new MarginPadding
+ {
+ Top = Direction.Value == ScrollingDirection.Down ? -Tail.Height : 0,
+ Bottom = Direction.Value == ScrollingDirection.Up ? -Tail.Height : 0,
+ };
+
+ // Pad the masking container to the starting position of the body piece (half-way under the head).
+ // This is required to make the body start getting masked immediately as soon as the note is held.
+ maskingContainer.Padding = new MarginPadding
+ {
+ Top = Direction.Value == ScrollingDirection.Up ? Head.Height / 2 : 0,
+ Bottom = Direction.Value == ScrollingDirection.Down ? Head.Height / 2 : 0,
+ };
+
+ // Position and resize the body to lie half-way under the head and the tail notes.
bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2;
bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2;
+
+ // As the note is being held, adjust the size of the sizing container. This has two effects:
+ // 1. The contained masking container will mask the body and ticks.
+ // 2. The head note will move along with the new "head position" in the container.
+ if (Head.IsHit && releaseTime == null)
+ {
+ // How far past the hit target this hold note is. Always a positive value.
+ float yOffset = Math.Max(0, Direction.Value == ScrollingDirection.Up ? -Y : Y);
+ sizingContainer.Height = Math.Clamp(1 - yOffset / DrawHeight, 0, 1);
+ }
}
protected override void UpdateStateTransforms(ArmedState state)
@@ -153,9 +238,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (Tail.AllJudged)
- ApplyResult(r => r.Type = HitResult.Perfect);
+ {
+ ApplyResult(r => r.Type = r.Judgement.MaxResult);
+ endHold();
+ }
- if (Tail.Result.Type == HitResult.Miss)
+ if (Tail.Judged && !Tail.IsHit)
HasBroken = true;
}
@@ -167,6 +255,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (action != Action.Value)
return false;
+ if (CheckHittable?.Invoke(this, Time.Current) == false)
+ return false;
+
// The tail has a lenience applied to it which is factored into the miss window (i.e. the miss judgement will be delayed).
// But the hold cannot ever be started within the late-lenience window, so we should skip trying to begin the hold during that time.
// Note: Unlike below, we use the tail's start time to determine the time offset.
@@ -206,6 +297,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// If the key has been released too early, the user should not receive full score for the release
if (!Tail.IsHit)
HasBroken = true;
+
+ releaseTime = Time.Current;
}
private void endHold()
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
index a73fe259e4..cd56b81e10 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Objects.Drawables;
+
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
///
@@ -17,6 +19,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public void UpdateResult() => base.UpdateResult(true);
+ protected override void UpdateStateTransforms(ArmedState state)
+ {
+ // This hitobject should never expire, so this is just a safe maximum.
+ LifetimeEnd = LifetimeStart + 30000;
+ }
+
public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note
public override void OnReleased(ManiaAction action)
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
index 31e43d3ee2..c780c0836e 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (!userTriggered)
{
if (!HitObject.HitWindows.CanBeHit(timeOffset))
- ApplyResult(r => r.Type = HitResult.Miss);
+ ApplyResult(r => r.Type = r.Judgement.MinResult);
return;
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
index 9b0322a6cd..f265419aa0 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
@@ -17,6 +16,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
public class DrawableHoldNoteTick : DrawableManiaHitObject
{
+ public override bool DisplayResult => false;
+
///
/// References the time at which the user started holding the hold note.
///
@@ -73,9 +74,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
var startTime = HoldStartTime?.Invoke();
if (startTime == null || startTime > HitObject.StartTime)
- ApplyResult(r => r.Type = HitResult.Miss);
+ ApplyResult(r => r.Type = r.Judgement.MinResult);
else
- ApplyResult(r => r.Type = HitResult.Perfect);
+ ApplyResult(r => r.Type = r.Judgement.MaxResult);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index a44d8b09aa..27960b3f3a 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -34,6 +35,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}
}
+ ///
+ /// Whether this can be hit, given a time value.
+ /// If non-null, judgements will be ignored whilst the function returns false.
+ ///
+ public Func CheckHittable;
+
protected DrawableManiaHitObject(ManiaHitObject hitObject)
: base(hitObject)
{
@@ -120,10 +127,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
break;
case ArmedState.Hit:
- this.FadeOut(150, Easing.OutQuint);
+ this.FadeOut();
break;
}
}
+
+ ///
+ /// Causes this to get missed, disregarding all conditions in implementations of .
+ ///
+ public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
}
public abstract class DrawableManiaHitObject : DrawableManiaHitObject
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
index 9451bc4430..b3402d13e4 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (!userTriggered)
{
if (!HitObject.HitWindows.CanBeHit(timeOffset))
- ApplyResult(r => r.Type = HitResult.Miss);
+ ApplyResult(r => r.Type = r.Judgement.MinResult);
return;
}
@@ -64,6 +64,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (action != Action.Value)
return false;
+ if (CheckHittable?.Invoke(this, Time.Current) == false)
+ return false;
+
return UpdateResult(true);
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs
index bc4a095395..9999983af5 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs
@@ -19,24 +19,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
///
/// Represents length-wise portion of a hold note.
///
- public class DefaultBodyPiece : CompositeDrawable
+ public class DefaultBodyPiece : CompositeDrawable, IHoldNoteBody
{
protected readonly Bindable AccentColour = new Bindable();
-
- private readonly LayoutValue subtractionCache = new LayoutValue(Invalidation.DrawSize);
- private readonly IBindable isHitting = new Bindable();
+ protected readonly IBindable IsHitting = new Bindable();
protected Drawable Background { get; private set; }
- protected BufferedContainer Foreground { get; private set; }
-
- private BufferedContainer subtractionContainer;
- private Container subtractionLayer;
+ private Container foregroundContainer;
public DefaultBodyPiece()
{
Blending = BlendingParameters.Additive;
-
- AddLayout(subtractionCache);
}
[BackgroundDependencyLoader(true)]
@@ -45,7 +38,54 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
InternalChildren = new[]
{
Background = new Box { RelativeSizeAxes = Axes.Both },
- Foreground = new BufferedContainer
+ foregroundContainer = new Container { RelativeSizeAxes = Axes.Both }
+ };
+
+ if (drawableObject != null)
+ {
+ var holdNote = (DrawableHoldNote)drawableObject;
+
+ AccentColour.BindTo(drawableObject.AccentColour);
+ IsHitting.BindTo(holdNote.IsHitting);
+ }
+
+ AccentColour.BindValueChanged(onAccentChanged, true);
+
+ Recycle();
+ }
+
+ public void Recycle() => foregroundContainer.Child = CreateForeground();
+
+ protected virtual Drawable CreateForeground() => new ForegroundPiece
+ {
+ AccentColour = { BindTarget = AccentColour },
+ IsHitting = { BindTarget = IsHitting }
+ };
+
+ private void onAccentChanged(ValueChangedEvent accent) => Background.Colour = accent.NewValue.Opacity(0.7f);
+
+ private class ForegroundPiece : CompositeDrawable
+ {
+ public readonly Bindable AccentColour = new Bindable();
+ public readonly IBindable IsHitting = new Bindable();
+
+ private readonly LayoutValue subtractionCache = new LayoutValue(Invalidation.DrawSize);
+
+ private BufferedContainer foregroundBuffer;
+ private BufferedContainer subtractionBuffer;
+ private Container subtractionLayer;
+
+ public ForegroundPiece()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ AddLayout(subtractionCache);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = foregroundBuffer = new BufferedContainer
{
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
@@ -53,7 +93,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
Children = new Drawable[]
{
new Box { RelativeSizeAxes = Axes.Both },
- subtractionContainer = new BufferedContainer
+ subtractionBuffer = new BufferedContainer
{
RelativeSizeAxes = Axes.Both,
// This is needed because we're blending with another object
@@ -77,60 +117,51 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
}
}
}
- }
- };
-
- if (drawableObject != null)
- {
- var holdNote = (DrawableHoldNote)drawableObject;
-
- AccentColour.BindTo(drawableObject.AccentColour);
- isHitting.BindTo(holdNote.IsHitting);
- }
-
- AccentColour.BindValueChanged(onAccentChanged, true);
- isHitting.BindValueChanged(_ => onAccentChanged(new ValueChangedEvent(AccentColour.Value, AccentColour.Value)), true);
- }
-
- private void onAccentChanged(ValueChangedEvent accent)
- {
- Foreground.Colour = accent.NewValue.Opacity(0.5f);
- Background.Colour = accent.NewValue.Opacity(0.7f);
-
- const float animation_length = 50;
-
- Foreground.ClearTransforms(false, nameof(Foreground.Colour));
-
- if (isHitting.Value)
- {
- // wait for the next sync point
- double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
- using (Foreground.BeginDelayedSequence(synchronisedOffset))
- Foreground.FadeColour(accent.NewValue.Lighten(0.2f), animation_length).Then().FadeColour(Foreground.Colour, animation_length).Loop();
- }
-
- subtractionCache.Invalidate();
- }
-
- protected override void Update()
- {
- base.Update();
-
- if (!subtractionCache.IsValid)
- {
- subtractionLayer.Width = 5;
- subtractionLayer.Height = Math.Max(0, DrawHeight - DrawWidth);
- subtractionLayer.EdgeEffect = new EdgeEffectParameters
- {
- Colour = Color4.White,
- Type = EdgeEffectType.Glow,
- Radius = DrawWidth
};
- Foreground.ForceRedraw();
- subtractionContainer.ForceRedraw();
+ AccentColour.BindValueChanged(onAccentChanged, true);
+ IsHitting.BindValueChanged(_ => onAccentChanged(new ValueChangedEvent(AccentColour.Value, AccentColour.Value)), true);
+ }
- subtractionCache.Validate();
+ private void onAccentChanged(ValueChangedEvent accent)
+ {
+ foregroundBuffer.Colour = accent.NewValue.Opacity(0.5f);
+
+ const float animation_length = 50;
+
+ foregroundBuffer.ClearTransforms(false, nameof(foregroundBuffer.Colour));
+
+ if (IsHitting.Value)
+ {
+ // wait for the next sync point
+ double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
+ using (foregroundBuffer.BeginDelayedSequence(synchronisedOffset))
+ foregroundBuffer.FadeColour(accent.NewValue.Lighten(0.2f), animation_length).Then().FadeColour(foregroundBuffer.Colour, animation_length).Loop();
+ }
+
+ subtractionCache.Invalidate();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!subtractionCache.IsValid)
+ {
+ subtractionLayer.Width = 5;
+ subtractionLayer.Height = Math.Max(0, DrawHeight - DrawWidth);
+ subtractionLayer.EdgeEffect = new EdgeEffectParameters
+ {
+ Colour = Color4.White,
+ Type = EdgeEffectType.Glow,
+ Radius = DrawWidth
+ };
+
+ foregroundBuffer.ForceRedraw();
+ subtractionBuffer.ForceRedraw();
+
+ subtractionCache.Validate();
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/IHoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/IHoldNoteBody.cs
new file mode 100644
index 0000000000..ac3792c01d
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/IHoldNoteBody.cs
@@ -0,0 +1,16 @@
+// 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.Drawables.Pieces
+{
+ ///
+ /// Interface for mania hold note bodies.
+ ///
+ public interface IHoldNoteBody
+ {
+ ///
+ /// Recycles the contents of this to free used resources.
+ ///
+ void Recycle();
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
index a100c9a58e..6cc7ff92d3 100644
--- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
@@ -102,14 +102,14 @@ namespace osu.Game.Rulesets.Mania.Objects
{
StartTime = StartTime,
Column = Column,
- Samples = getNodeSamples(0),
+ Samples = GetNodeSamples(0),
});
AddNested(Tail = new TailNote
{
StartTime = EndTime,
Column = Column,
- Samples = getNodeSamples((NodeSamples?.Count - 1) ?? 1),
+ Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1),
});
}
@@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mania.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
- private IList getNodeSamples(int nodeIndex) =>
+ public IList GetNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
}
}
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
index 483327d5b3..3ebbe5af8e 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
@@ -46,6 +46,9 @@ namespace osu.Game.Rulesets.Mania.Replays
public override Replay Generate()
{
+ if (Beatmap.HitObjects.Count == 0)
+ return Replay;
+
var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time);
var actions = new List();
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider-expected-conversion.json
new file mode 100644
index 0000000000..229760cd1c
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider-expected-conversion.json
@@ -0,0 +1,14 @@
+{
+ "Mappings": [{
+ "RandomW": 3083084786,
+ "RandomX": 273326509,
+ "RandomY": 273553282,
+ "RandomZ": 2659838971,
+ "StartTime": 4836,
+ "Objects": [{
+ "StartTime": 4836,
+ "EndTime": 4836,
+ "Column": 0
+ }]
+ }]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider.osu
new file mode 100644
index 0000000000..9b8ac1f9db
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider.osu
@@ -0,0 +1,20 @@
+osu file format v14
+
+[General]
+StackLeniency: 0.7
+Mode: 0
+
+[Difficulty]
+HPDrainRate:1
+CircleSize:4
+OverallDifficulty:1
+ApproachRate:9
+SliderMultiplier:2.5
+SliderTickRate:0.5
+
+[TimingPoints]
+34,431.654676258993,4,1,0,50,1,0
+4782,-66.6666666666667,4,1,0,20,0,0
+
+[HitObjects]
+15,199,4836,22,0,L,1,46.8750017881394
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
index 4b2f643333..71cc0bdf1f 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
@@ -10,7 +10,5 @@ namespace osu.Game.Rulesets.Mania.Scoring
protected override double DefaultAccuracyPortion => 0.95;
protected override double DefaultComboPortion => 0.05;
-
- public override HitWindows CreateHitWindows() => new ManiaHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs b/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs
new file mode 100644
index 0000000000..c8b05ed2f8
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.Skinning
+{
+ public class HitTargetInsetContainer : Container
+ {
+ private readonly IBindable direction = new Bindable();
+
+ protected override Container Content => content;
+ private readonly Container content;
+
+ private float hitPosition;
+
+ public HitTargetInsetContainer()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChild = content = new Container { RelativeSizeAxes = Axes.Both };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
+ {
+ hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? Stage.HIT_TARGET_POSITION;
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ content.Padding = direction.NewValue == ScrollingDirection.Up
+ ? new MarginPadding { Top = hitPosition }
+ : new MarginPadding { Bottom = hitPosition };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
index 9f716428c0..c0f0fcb4af 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -19,7 +21,14 @@ namespace osu.Game.Rulesets.Mania.Skinning
private readonly IBindable direction = new Bindable();
private readonly IBindable isHitting = new Bindable();
- private Drawable sprite;
+ [CanBeNull]
+ private Drawable bodySprite;
+
+ [CanBeNull]
+ private Drawable lightContainer;
+
+ [CanBeNull]
+ private Drawable light;
public LegacyBodyPiece()
{
@@ -32,7 +41,39 @@ namespace osu.Game.Rulesets.Mania.Skinning
string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
?? $"mania-note{FallbackColumnIndex}L";
- sprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d =>
+ string lightImage = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightImage)?.Value
+ ?? "lightingL";
+
+ float lightScale = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightScale)?.Value
+ ?? 1;
+
+ // Create a temporary animation to retrieve the number of frames, in an effort to calculate the intended frame length.
+ // This animation is discarded and re-queried with the appropriate frame length afterwards.
+ var tmp = skin.GetAnimation(lightImage, true, false);
+ double frameLength = 0;
+ if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
+ frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount);
+
+ light = skin.GetAnimation(lightImage, true, true, frameLength: frameLength).With(d =>
+ {
+ if (d == null)
+ return;
+
+ d.Origin = Anchor.Centre;
+ d.Blending = BlendingParameters.Additive;
+ d.Scale = new Vector2(lightScale);
+ });
+
+ if (light != null)
+ {
+ lightContainer = new HitTargetInsetContainer
+ {
+ Alpha = 0,
+ Child = light
+ };
+ }
+
+ bodySprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d =>
{
if (d == null)
return;
@@ -47,8 +88,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
// Todo: Wrap
});
- if (sprite != null)
- InternalChild = sprite;
+ if (bodySprite != null)
+ InternalChild = bodySprite;
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
@@ -60,28 +101,68 @@ namespace osu.Game.Rulesets.Mania.Skinning
private void onIsHittingChanged(ValueChangedEvent isHitting)
{
- if (!(sprite is TextureAnimation animation))
+ if (bodySprite is TextureAnimation bodyAnimation)
+ {
+ bodyAnimation.GotoFrame(0);
+ bodyAnimation.IsPlaying = isHitting.NewValue;
+ }
+
+ if (lightContainer == null)
return;
- animation.GotoFrame(0);
- animation.IsPlaying = isHitting.NewValue;
+ if (isHitting.NewValue)
+ {
+ // Clear the fade out and, more importantly, the removal.
+ lightContainer.ClearTransforms();
+
+ // Only add the container if the removal has taken place.
+ if (lightContainer.Parent == null)
+ Column.TopLevelContainer.Add(lightContainer);
+
+ // The light must be seeked only after being loaded, otherwise a nullref occurs (https://github.com/ppy/osu-framework/issues/3847).
+ if (light is TextureAnimation lightAnimation)
+ lightAnimation.GotoFrame(0);
+
+ lightContainer.FadeIn(80);
+ }
+ else
+ {
+ lightContainer.FadeOut(120)
+ .OnComplete(d => Column.TopLevelContainer.Remove(d));
+ }
}
private void onDirectionChanged(ValueChangedEvent direction)
{
- if (sprite == null)
- return;
-
if (direction.NewValue == ScrollingDirection.Up)
{
- sprite.Origin = Anchor.BottomCentre;
- sprite.Scale = new Vector2(1, -1);
+ if (bodySprite != null)
+ {
+ bodySprite.Origin = Anchor.BottomCentre;
+ bodySprite.Scale = new Vector2(1, -1);
+ }
+
+ if (light != null)
+ light.Anchor = Anchor.TopCentre;
}
else
{
- sprite.Origin = Anchor.TopCentre;
- sprite.Scale = Vector2.One;
+ if (bodySprite != null)
+ {
+ bodySprite.Origin = Anchor.TopCentre;
+ bodySprite.Scale = Vector2.One;
+ }
+
+ if (light != null)
+ light.Anchor = Anchor.BottomCentre;
}
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ lightContainer?.Expire();
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs
index 64a7641421..3bf51b3073 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs
@@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.UI.Scrolling;
@@ -18,14 +17,12 @@ namespace osu.Game.Rulesets.Mania.Skinning
public class LegacyColumnBackground : LegacyManiaColumnElement, IKeyBindingHandler
{
private readonly IBindable direction = new Bindable();
- private readonly bool isLastColumn;
private Container lightContainer;
private Sprite light;
- public LegacyColumnBackground(bool isLastColumn)
+ public LegacyColumnBackground()
{
- this.isLastColumn = isLastColumn;
RelativeSizeAxes = Axes.Both;
}
@@ -35,52 +32,14 @@ namespace osu.Game.Rulesets.Mania.Skinning
string lightImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LightImage)?.Value
?? "mania-stage-light";
- float leftLineWidth = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftLineWidth)
- ?.Value ?? 1;
- float rightLineWidth = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.RightLineWidth)
- ?.Value ?? 1;
-
- bool hasLeftLine = leftLineWidth > 0;
- bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m
- || isLastColumn;
-
float lightPosition = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LightPosition)?.Value
?? 0;
- Color4 lineColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLineColour)?.Value
- ?? Color4.White;
-
- Color4 backgroundColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour)?.Value
- ?? Color4.Black;
-
Color4 lightColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value
?? Color4.White;
- InternalChildren = new Drawable[]
+ InternalChildren = new[]
{
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = backgroundColour
- },
- new Box
- {
- RelativeSizeAxes = Axes.Y,
- Width = leftLineWidth,
- Scale = new Vector2(0.740f, 1),
- Colour = lineColour,
- Alpha = hasLeftLine ? 1 : 0
- },
- new Box
- {
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- RelativeSizeAxes = Axes.Y,
- Width = rightLineWidth,
- Scale = new Vector2(0.740f, 1),
- Colour = lineColour,
- Alpha = hasRightLine ? 1 : 0
- },
lightContainer = new Container
{
Origin = Anchor.BottomCentre,
@@ -90,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
- Colour = lightColour,
+ Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour),
Texture = skin.GetTexture(lightImage),
RelativeSizeAxes = Axes.X,
Width = 1,
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
index 12747924de..7c5d41efcf 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
@@ -66,6 +67,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
public void Animate(JudgementResult result)
{
+ if (result.Judgement is HoldNoteTickJudgement)
+ return;
+
(explosion as IFramedAnimation)?.GotoFrame(0);
explosion?.FadeInFromZero(80)
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs
index d055ef3480..6eced571d2 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs
@@ -20,11 +20,6 @@ namespace osu.Game.Rulesets.Mania.Skinning
private Container directionContainer;
- public LegacyHitTarget()
- {
- RelativeSizeAxes = Axes.Both;
- }
-
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
{
@@ -56,7 +51,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
Anchor = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
Height = 1,
- Colour = lineColour,
+ Colour = LegacyColourCompatibility.DisallowZeroAlpha(lineColour),
Alpha = showJudgementLine ? 0.9f : 0
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs
index 44f3e7d7b3..b269ea25d4 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs
@@ -65,6 +65,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
+
+ if (GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeysUnderNotes)?.Value ?? false)
+ Column.UnderlayElements.Add(CreateProxy());
}
private void onDirectionChanged(ValueChangedEvent direction)
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs
index 7f5de601ca..b0bab8e760 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs
@@ -4,19 +4,27 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.UI;
using osu.Game.Skinning;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning
{
public class LegacyStageBackground : CompositeDrawable
{
+ private readonly StageDefinition stageDefinition;
+
private Drawable leftSprite;
private Drawable rightSprite;
+ private ColumnFlow columnBackgrounds;
- public LegacyStageBackground()
+ public LegacyStageBackground(StageDefinition stageDefinition)
{
+ this.stageDefinition = stageDefinition;
RelativeSizeAxes = Axes.Both;
}
@@ -44,8 +52,19 @@ namespace osu.Game.Rulesets.Mania.Skinning
Origin = Anchor.TopLeft,
X = -0.05f,
Texture = skin.GetTexture(rightImage)
+ },
+ columnBackgrounds = new ColumnFlow(stageDefinition)
+ {
+ RelativeSizeAxes = Axes.Y
+ },
+ new HitTargetInsetContainer
+ {
+ Child = new LegacyHitTarget { RelativeSizeAxes = Axes.Both }
}
};
+
+ for (int i = 0; i < stageDefinition.Columns; i++)
+ columnBackgrounds.SetContentForColumn(i, new ColumnBackground(i, i == stageDefinition.Columns - 1));
}
protected override void Update()
@@ -58,5 +77,72 @@ namespace osu.Game.Rulesets.Mania.Skinning
if (rightSprite?.Height > 0)
rightSprite.Scale = new Vector2(1, DrawHeight / rightSprite.Height);
}
+
+ private class ColumnBackground : CompositeDrawable
+ {
+ private readonly int columnIndex;
+ private readonly bool isLastColumn;
+
+ public ColumnBackground(int columnIndex, bool isLastColumn)
+ {
+ this.columnIndex = columnIndex;
+ this.isLastColumn = isLastColumn;
+
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ float leftLineWidth = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LeftLineWidth, columnIndex)?.Value ?? 1;
+ float rightLineWidth = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.RightLineWidth, columnIndex)?.Value ?? 1;
+
+ bool hasLeftLine = leftLineWidth > 0;
+ bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m
+ || isLastColumn;
+
+ Color4 lineColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnLineColour, columnIndex)?.Value ?? Color4.White;
+ Color4 backgroundColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour, columnIndex)?.Value ?? Color4.Black;
+
+ InternalChildren = new Drawable[]
+ {
+ LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ }, backgroundColour),
+ new HitTargetInsetContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.Y,
+ Width = leftLineWidth,
+ Scale = new Vector2(0.740f, 1),
+ Alpha = hasLeftLine ? 1 : 0,
+ Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ }, lineColour)
+ },
+ new Container
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ RelativeSizeAxes = Axes.Y,
+ Width = rightLineWidth,
+ Scale = new Vector2(0.740f, 1),
+ Alpha = hasRightLine ? 1 : 0,
+ Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ }, lineColour)
+ },
+ }
+ }
+ };
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
index e167135556..3724269f4d 100644
--- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs
@@ -9,6 +9,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Skinning;
using System.Collections.Generic;
+using System.Diagnostics;
using osu.Framework.Audio.Sample;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Legacy;
@@ -88,10 +89,12 @@ namespace osu.Game.Rulesets.Mania.Skinning
switch (maniaComponent.Component)
{
case ManiaSkinComponents.ColumnBackground:
- return new LegacyColumnBackground(maniaComponent.TargetColumn == beatmap.TotalColumns - 1);
+ return new LegacyColumnBackground();
case ManiaSkinComponents.HitTarget:
- return new LegacyHitTarget();
+ // Legacy skins sandwich the hit target between the column background and the column light.
+ // To preserve this ordering, it's created manually inside LegacyStageBackground.
+ return Drawable.Empty();
case ManiaSkinComponents.KeyArea:
return new LegacyKeyArea();
@@ -112,7 +115,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
return new LegacyHitExplosion();
case ManiaSkinComponents.StageBackground:
- return new LegacyStageBackground();
+ Debug.Assert(maniaComponent.StageDefinition != null);
+ return new LegacyStageBackground(maniaComponent.StageDefinition.Value);
case ManiaSkinComponents.StageForeground:
return new LegacyStageForeground();
@@ -126,6 +130,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
private Drawable getResult(HitResult result)
{
+ if (!hitresult_mapping.ContainsKey(result))
+ return null;
+
string filename = this.GetManiaSkinConfig(hitresult_mapping[result])?.Value
?? default_hitresult_skin_filenames[result];
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 255ce4c064..c28a1c13d8 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -17,6 +17,7 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -36,6 +37,7 @@ namespace osu.Game.Rulesets.Mania.UI
public readonly ColumnHitObjectArea HitObjectArea;
internal readonly Container TopLevelContainer;
private readonly DrawablePool hitExplosionPool;
+ private readonly OrderedHitPolicy hitPolicy;
public Container UnderlayElements => HitObjectArea.UnderlayElements;
@@ -65,11 +67,11 @@ namespace osu.Game.Rulesets.Mania.UI
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
};
+ hitPolicy = new OrderedHitPolicy(HitObjectContainer);
+
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
}
- public override Axes RelativeSizeAxes => Axes.Y;
-
public ColumnType ColumnType { get; set; }
public bool IsSpecial => ColumnType == ColumnType.Special;
@@ -92,6 +94,9 @@ namespace osu.Game.Rulesets.Mania.UI
hitObject.AccentColour.Value = AccentColour;
hitObject.OnNewResult += OnNewResult;
+ DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject;
+ maniaObject.CheckHittable = hitPolicy.IsHittable;
+
HitObjectContainer.Add(hitObject);
}
@@ -106,7 +111,10 @@ namespace osu.Game.Rulesets.Mania.UI
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
{
- if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
+ if (result.IsHit)
+ hitPolicy.HandleHit(judgedObject);
+
+ if (!result.IsHit || !DisplayJudgements.Value)
return;
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
new file mode 100644
index 0000000000..aef82d4c08
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
@@ -0,0 +1,105 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Skinning;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.UI
+{
+ ///
+ /// A which flows its contents according to the s in a .
+ /// Content can be added to individual columns via .
+ ///
+ /// The type of content in each column.
+ public class ColumnFlow : CompositeDrawable
+ where TContent : Drawable
+ {
+ ///
+ /// All contents added to this .
+ ///
+ public IReadOnlyList Content => columns.Children.Select(c => c.Count == 0 ? null : (TContent)c.Child).ToList();
+
+ private readonly FillFlowContainer columns;
+ private readonly StageDefinition stageDefinition;
+
+ public ColumnFlow(StageDefinition stageDefinition)
+ {
+ this.stageDefinition = stageDefinition;
+
+ AutoSizeAxes = Axes.X;
+
+ InternalChild = columns = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Y,
+ AutoSizeAxes = Axes.X,
+ Direction = FillDirection.Horizontal,
+ };
+
+ for (int i = 0; i < stageDefinition.Columns; i++)
+ columns.Add(new Container { RelativeSizeAxes = Axes.Y });
+ }
+
+ private ISkinSource currentSkin;
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ currentSkin = skin;
+
+ skin.SourceChanged += onSkinChanged;
+ onSkinChanged();
+ }
+
+ private void onSkinChanged()
+ {
+ for (int i = 0; i < stageDefinition.Columns; i++)
+ {
+ if (i > 0)
+ {
+ float spacing = currentSkin.GetConfig(
+ new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1))
+ ?.Value ?? Stage.COLUMN_SPACING;
+
+ columns[i].Margin = new MarginPadding { Left = spacing };
+ }
+
+ float? width = currentSkin.GetConfig(
+ new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i))
+ ?.Value;
+
+ if (width == null)
+ // only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration)
+ columns[i].Width = stageDefinition.IsSpecialColumn(i) ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH;
+ else
+ columns[i].Width = width.Value;
+ }
+ }
+
+ ///
+ /// Sets the content of one of the columns of this .
+ ///
+ /// The index of the column to set the content of.
+ /// The content.
+ public void SetContentForColumn(int column, TContent content) => columns[column].Child = content;
+
+ public new MarginPadding Padding
+ {
+ get => base.Padding;
+ set => base.Padding = value;
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (currentSkin != null)
+ currentSkin.SourceChanged -= onSkinChanged;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index 94b5ee9486..7f5b9a6ee0 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -31,12 +31,12 @@ namespace osu.Game.Rulesets.Mania.UI
///
/// The minimum time range. This occurs at a of 40.
///
- public const double MIN_TIME_RANGE = 150;
+ public const double MIN_TIME_RANGE = 340;
///
/// The maximum time range. This occurs at a of 1.
///
- public const double MAX_TIME_RANGE = 6000;
+ public const double MAX_TIME_RANGE = 13720;
protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
diff --git a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs
new file mode 100644
index 0000000000..0f9cd48dd8
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs
@@ -0,0 +1,78 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Mania.UI
+{
+ ///
+ /// Ensures that only the most recent is hittable, affectionately known as "note lock".
+ ///
+ public class OrderedHitPolicy
+ {
+ private readonly HitObjectContainer hitObjectContainer;
+
+ public OrderedHitPolicy(HitObjectContainer hitObjectContainer)
+ {
+ this.hitObjectContainer = hitObjectContainer;
+ }
+
+ ///
+ /// Determines whether a can be hit at a point in time.
+ ///
+ ///
+ /// Only the most recent can be hit, a previous hitobject's window cannot extend past the next one.
+ ///
+ /// The to check.
+ /// The time to check.
+ /// Whether can be hit at the given .
+ public bool IsHittable(DrawableHitObject hitObject, double time)
+ {
+ var nextObject = hitObjectContainer.AliveObjects.GetNext(hitObject);
+ return nextObject == null || time < nextObject.HitObject.StartTime;
+ }
+
+ ///
+ /// Handles a being hit to potentially miss all earlier s.
+ ///
+ /// The that was hit.
+ public void HandleHit(DrawableHitObject hitObject)
+ {
+ if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset))
+ throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
+
+ foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
+ {
+ if (obj.Judged)
+ continue;
+
+ ((DrawableManiaHitObject)obj).MissForcefully();
+ }
+ }
+
+ private IEnumerable enumerateHitObjectsUpTo(double targetTime)
+ {
+ foreach (var obj in hitObjectContainer.AliveObjects)
+ {
+ if (obj.HitObject.GetEndTime() >= targetTime)
+ yield break;
+
+ yield return obj;
+
+ foreach (var nestedObj in obj.NestedHitObjects)
+ {
+ if (nestedObj.HitObject.GetEndTime() >= targetTime)
+ break;
+
+ yield return nestedObj;
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
index 36780b0f80..e7a2de266d 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
@@ -11,7 +10,6 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
@@ -31,14 +29,13 @@ namespace osu.Game.Rulesets.Mania.UI
public const float HIT_TARGET_POSITION = 110;
- public IReadOnlyList Columns => columnFlow.Children;
- private readonly FillFlowContainer columnFlow;
+ public IReadOnlyList Columns => columnFlow.Content;
+ private readonly ColumnFlow columnFlow;
private readonly JudgementContainer judgements;
private readonly DrawablePool judgementPool;
private readonly Drawable barLineContainer;
- private readonly Container topLevelContainer;
private readonly Dictionary columnColours = new Dictionary
{
@@ -62,6 +59,8 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;
+ Container topLevelContainer;
+
InternalChildren = new Drawable[]
{
judgementPool = new DrawablePool(2),
@@ -73,17 +72,13 @@ namespace osu.Game.Rulesets.Mania.UI
AutoSizeAxes = Axes.X,
Children = new Drawable[]
{
- new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground())
+ new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: definition), _ => new DefaultStageBackground())
{
RelativeSizeAxes = Axes.Both
},
- columnFlow = new FillFlowContainer
+ columnFlow = new ColumnFlow(definition)
{
- Name = "Columns",
RelativeSizeAxes = Axes.Y,
- AutoSizeAxes = Axes.X,
- Direction = FillDirection.Horizontal,
- Padding = new MarginPadding { Left = COLUMN_SPACING, Right = COLUMN_SPACING },
},
new Container
{
@@ -102,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y,
}
},
- new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null)
+ new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: definition), _ => null)
{
RelativeSizeAxes = Axes.Both
},
@@ -121,60 +116,22 @@ namespace osu.Game.Rulesets.Mania.UI
for (int i = 0; i < definition.Columns; i++)
{
var columnType = definition.GetTypeOfColumn(i);
+
var column = new Column(firstColumnIndex + i)
{
+ RelativeSizeAxes = Axes.Both,
+ Width = 1,
ColumnType = columnType,
AccentColour = columnColours[columnType],
Action = { Value = columnType == ColumnType.Special ? specialColumnStartAction++ : normalColumnStartAction++ }
};
- AddColumn(column);
+ topLevelContainer.Add(column.TopLevelContainer.CreateProxy());
+ columnFlow.SetContentForColumn(i, column);
+ AddNested(column);
}
}
- private ISkin currentSkin;
-
- [BackgroundDependencyLoader]
- private void load(ISkinSource skin)
- {
- currentSkin = skin;
- skin.SourceChanged += onSkinChanged;
-
- onSkinChanged();
- }
-
- private void onSkinChanged()
- {
- foreach (var col in columnFlow)
- {
- if (col.Index > 0)
- {
- float spacing = currentSkin.GetConfig(
- new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, col.Index - 1))
- ?.Value ?? COLUMN_SPACING;
-
- col.Margin = new MarginPadding { Left = spacing };
- }
-
- float? width = currentSkin.GetConfig(
- new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, col.Index))
- ?.Value;
-
- if (width == null)
- // only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration)
- col.Width = col.IsSpecial ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH;
- else
- col.Width = width.Value;
- }
- }
-
- public void AddColumn(Column c)
- {
- topLevelContainer.Add(c.TopLevelContainer.CreateProxy());
- columnFlow.Add(c);
- AddNested(c);
- }
-
public override void Add(DrawableHitObject h)
{
var maniaObject = (ManiaHitObject)h.HitObject;
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs
similarity index 94%
rename from osu.Game.Rulesets.Osu.Tests/TestSceneHitCirclePlacementBlueprint.cs
rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs
index 4c6abc45f7..7bccec6c97 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCirclePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
-namespace osu.Game.Rulesets.Osu.Tests
+namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCircleSelectionBlueprint.cs
similarity index 98%
rename from osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleSelectionBlueprint.cs
rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCircleSelectionBlueprint.cs
index 0ecce42e88..66cd405195 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCircleSelectionBlueprint.cs
@@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using osuTK;
-namespace osu.Game.Rulesets.Osu.Tests
+namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneHitCircleSelectionBlueprint : SelectionBlueprintTestScene
{
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs
new file mode 100644
index 0000000000..1ca94df26b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs
@@ -0,0 +1,90 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor
+{
+ [TestFixture]
+ public class TestSceneObjectObjectSnap : TestSceneOsuEditor
+ {
+ private OsuPlayfield playfield;
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false);
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+ AddStep("get playfield", () => playfield = Editor.ChildrenOfType().First());
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestHitCircleSnapsToOtherHitCircle(bool distanceSnapEnabled)
+ {
+ AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre));
+
+ if (!distanceSnapEnabled)
+ AddStep("disable distance snap", () => InputManager.Key(Key.Q));
+
+ AddStep("enter placement mode", () => InputManager.Key(Key.Number2));
+
+ AddStep("place first object", () => InputManager.Click(MouseButton.Left));
+
+ AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.02f, 0)));
+
+ AddStep("place second object", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("both objects at same location", () =>
+ {
+ var objects = EditorBeatmap.HitObjects;
+
+ var first = (OsuHitObject)objects.First();
+ var second = (OsuHitObject)objects.Last();
+
+ return first.Position == second.Position;
+ });
+ }
+
+ [Test]
+ public void TestHitCircleSnapsToSliderEnd()
+ {
+ AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre));
+
+ AddStep("disable distance snap", () => InputManager.Key(Key.Q));
+
+ AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3));
+
+ AddStep("start slider placement", () => InputManager.Click(MouseButton.Left));
+
+ AddStep("move to place end", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.185f, 0)));
+
+ AddStep("end slider placement", () => InputManager.Click(MouseButton.Right));
+
+ AddStep("enter circle placement mode", () => InputManager.Key(Key.Number2));
+
+ AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.20f, 0)));
+
+ AddStep("place second object", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("circle is at slider's end", () =>
+ {
+ var objects = EditorBeatmap.HitObjects;
+
+ var first = (Slider)objects.First();
+ var second = (OsuHitObject)objects.Last();
+
+ return Precision.AlmostEquals(first.EndPosition, second.Position);
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
similarity index 99%
rename from osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
index 0d0be2953b..1232369a0b 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
@@ -19,7 +19,7 @@ using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Rulesets.Osu.Tests
+namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneOsuDistanceSnapGrid : OsuManualInputManagerTestScene
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneEditor.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs
similarity index 76%
rename from osu.Game.Rulesets.Osu.Tests/TestSceneEditor.cs
rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs
index 9239034a53..e1ca3ddd61 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneEditor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs
@@ -4,10 +4,10 @@
using NUnit.Framework;
using osu.Game.Tests.Visual;
-namespace osu.Game.Rulesets.Osu.Tests
+namespace osu.Game.Rulesets.Osu.Tests.Editor
{
[TestFixture]
- public class TestSceneEditor : EditorTestScene
+ public class TestSceneOsuEditor : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs
similarity index 97%
rename from osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs
rename to osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs
index 21fa283b6d..738a21b17e 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs
@@ -12,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Visual;
using osuTK;
-namespace osu.Game.Rulesets.Osu.Tests
+namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestScenePathControlPointVisualiser : OsuTestScene
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs
similarity index 99%
rename from osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs
rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs
index fe9973f4d8..49d7d9249c 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs
@@ -14,7 +14,7 @@ using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
-namespace osu.Game.Rulesets.Osu.Tests
+namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs
similarity index 99%
rename from osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs
rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs
index d5be538d94..f6e1be693b 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs
@@ -16,7 +16,7 @@ using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
-namespace osu.Game.Rulesets.Osu.Tests
+namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneSliderSelectionBlueprint : SelectionBlueprintTestScene
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs
similarity index 94%
rename from osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerPlacementBlueprint.cs
rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs
index d74d072857..fa6c660b01 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
-namespace osu.Game.Rulesets.Osu.Tests
+namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneSpinnerPlacementBlueprint : PlacementBlueprintTestScene
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs
similarity index 96%
rename from osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSelectionBlueprint.cs
rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs
index 011463ab14..4248f68a60 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs
@@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using osuTK;
-namespace osu.Game.Rulesets.Osu.Tests
+namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneSpinnerSelectionBlueprint : SelectionBlueprintTestScene
{
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
new file mode 100644
index 0000000000..d8064d36ea
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
@@ -0,0 +1,65 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModSpunOut : OsuModTestScene
+ {
+ protected override bool AllowFail => true;
+
+ [Test]
+ public void TestSpinnerAutoCompleted() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSpunOut(),
+ Autoplay = false,
+ Beatmap = singleSpinnerBeatmap,
+ PassCondition = () => Player.ChildrenOfType().Single().Progress >= 1
+ });
+
+ [TestCase(null)]
+ [TestCase(typeof(OsuModDoubleTime))]
+ [TestCase(typeof(OsuModHalfTime))]
+ public void TestSpinRateUnaffectedByMods(Type additionalModType)
+ {
+ var mods = new List { new OsuModSpunOut() };
+ if (additionalModType != null)
+ mods.Add((Mod)Activator.CreateInstance(additionalModType));
+
+ CreateModTest(new ModTestData
+ {
+ Mods = mods,
+ Autoplay = false,
+ Beatmap = singleSpinnerBeatmap,
+ PassCondition = () => Precision.AlmostEquals(Player.ChildrenOfType().Single().SpinsPerMinute, 286, 1)
+ });
+ }
+
+ private Beatmap singleSpinnerBeatmap => new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Spinner
+ {
+ Position = new Vector2(256, 192),
+ StartTime = 500,
+ Duration = 2000
+ }
+ }
+ };
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs
index cd3daf18a9..7d32895083 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs
@@ -12,6 +12,7 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
+ [Timeout(10000)]
public class OsuBeatmapConversionTest : BeatmapConversionTest
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircle@2x.png
new file mode 100644
index 0000000000..c6c3771593
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircle@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircleoverlay@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircleoverlay@2x.png
new file mode 100644
index 0000000000..232560a1d4
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircleoverlay@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-background@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-background@2x.png
new file mode 100644
index 0000000000..4f50f638c5
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-background@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-circle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-circle@2x.png
new file mode 100644
index 0000000000..daf28e09cb
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-circle@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-metre@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-metre@2x.png
new file mode 100644
index 0000000000..6ef1068420
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-metre@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
index 37df0d6e37..596bc06c68 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
@@ -20,7 +20,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
private int depthIndex;
- public TestSceneHitCircle()
+ [Test]
+ public void TestVariousHitCircles()
{
AddStep("Miss Big Single", () => SetContents(() => testSingle(2)));
AddStep("Miss Medium Single", () => SetContents(() => testSingle(5)));
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
index f3221ffe32..39deba2f57 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
HitObjects = { new HitCircle { Position = new Vector2(256, 192) } }
},
- PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < -hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss
+ PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < -hitWindows.WindowFor(HitResult.Meh) && !Player.Results[0].IsHit
});
}
@@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
Autoplay = false,
Beatmap = beatmap,
- PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset >= hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss
+ PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset >= hitWindows.WindowFor(HitResult.Meh) && !Player.Results[0].IsHit
});
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
index 854626d362..32a36ab317 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
@@ -209,9 +209,9 @@ namespace osu.Game.Rulesets.Osu.Tests
});
addJudgementAssert(hitObjects[0], HitResult.Great);
- addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.IgnoreHit);
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Miss);
- addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great);
+ addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
}
///
@@ -252,9 +252,9 @@ namespace osu.Game.Rulesets.Osu.Tests
});
addJudgementAssert(hitObjects[0], HitResult.Great);
- addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.IgnoreHit);
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great);
- addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great);
+ addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
}
///
@@ -331,7 +331,7 @@ namespace osu.Game.Rulesets.Osu.Tests
});
addJudgementAssert(hitObjects[0], HitResult.Great);
- addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.IgnoreHit);
}
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index 6a689a1f80..c9e112f76d 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
private int depthIndex;
- public TestSceneSlider()
+ [Test]
+ public void TestVariousSliders()
{
AddStep("Big Single", () => SetContents(() => testSimpleBig()));
AddStep("Medium Single", () => SetContents(() => testSimpleMedium()));
@@ -164,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
- StartTime = Time.Current + 1000,
+ StartTime = Time.Current + time_offset,
Position = new Vector2(239, 176),
Path = new SliderPath(PathType.PerfectCurve, new[]
{
@@ -185,22 +186,26 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable testSlowSpeed() => createSlider(speedMultiplier: 0.5);
- private Drawable testShortSlowSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 0.5);
+ private Drawable testShortSlowSpeed(int repeats = 0) => createSlider(distance: max_length / 4, repeats: repeats, speedMultiplier: 0.5);
private Drawable testHighSpeed(int repeats = 0) => createSlider(repeats: repeats, speedMultiplier: 15);
- private Drawable testShortHighSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 15);
+ private Drawable testShortHighSpeed(int repeats = 0) => createSlider(distance: max_length / 4, repeats: repeats, speedMultiplier: 15);
- private Drawable createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0)
+ private const double time_offset = 1500;
+
+ private const float max_length = 200;
+
+ private Drawable createSlider(float circleSize = 2, float distance = max_length, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0)
{
var slider = new Slider
{
- StartTime = Time.Current + 1000,
- Position = new Vector2(-(distance / 2), 0),
+ StartTime = Time.Current + time_offset,
+ Position = new Vector2(0, -(distance / 2)),
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
- new Vector2(distance, 0),
+ new Vector2(0, distance),
}, distance),
RepeatCount = repeats,
StackHeight = stackHeight
@@ -213,14 +218,14 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
- StartTime = Time.Current + 1000,
- Position = new Vector2(-200, 0),
+ StartTime = Time.Current + time_offset,
+ Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
- new Vector2(200, 200),
- new Vector2(400, 0)
- }, 600),
+ new Vector2(max_length / 2, max_length / 2),
+ new Vector2(max_length, 0)
+ }, max_length * 1.5f),
RepeatCount = repeats,
};
@@ -233,16 +238,16 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
- StartTime = Time.Current + 1000,
- Position = new Vector2(-200, 0),
+ StartTime = Time.Current + time_offset,
+ Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
- new Vector2(150, 75),
- new Vector2(200, 0),
- new Vector2(300, -200),
- new Vector2(400, 0),
- new Vector2(430, 0)
+ new Vector2(max_length * 0.375f, max_length * 0.18f),
+ new Vector2(max_length / 2, 0),
+ new Vector2(max_length * 0.75f, -max_length / 2),
+ new Vector2(max_length * 0.95f, 0),
+ new Vector2(max_length, 0)
}),
RepeatCount = repeats,
};
@@ -256,15 +261,15 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
- StartTime = Time.Current + 1000,
- Position = new Vector2(-200, 0),
+ StartTime = Time.Current + time_offset,
+ Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.Bezier, new[]
{
Vector2.Zero,
- new Vector2(150, 75),
- new Vector2(200, 100),
- new Vector2(300, -200),
- new Vector2(430, 0)
+ new Vector2(max_length * 0.375f, max_length * 0.18f),
+ new Vector2(max_length / 2, max_length / 4),
+ new Vector2(max_length * 0.75f, -max_length / 2),
+ new Vector2(max_length, 0)
}),
RepeatCount = repeats,
};
@@ -278,16 +283,16 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
- StartTime = Time.Current + 1000,
+ StartTime = Time.Current + time_offset,
Position = new Vector2(0, 0),
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
- new Vector2(-200, 0),
+ new Vector2(-max_length / 2, 0),
new Vector2(0, 0),
- new Vector2(0, -200),
- new Vector2(-200, -200),
- new Vector2(0, -200)
+ new Vector2(0, -max_length / 2),
+ new Vector2(-max_length / 2, -max_length / 2),
+ new Vector2(0, -max_length / 2)
}),
RepeatCount = repeats,
};
@@ -305,14 +310,14 @@ namespace osu.Game.Rulesets.Osu.Tests
var slider = new Slider
{
- StartTime = Time.Current + 1000,
- Position = new Vector2(-100, 0),
+ StartTime = Time.Current + time_offset,
+ Position = new Vector2(-max_length / 4, 0),
Path = new SliderPath(PathType.Catmull, new[]
{
Vector2.Zero,
- new Vector2(50, -50),
- new Vector2(150, 50),
- new Vector2(200, 0)
+ new Vector2(max_length * 0.125f, max_length * 0.125f),
+ new Vector2(max_length * 0.375f, max_length * 0.125f),
+ new Vector2(max_length / 2, 0)
}),
RepeatCount = repeats,
NodeSamples = repeatSamples
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
index b543b6fa94..0164fb8bf4 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_2 },
});
- AddAssert("Tracking retained", assertGreatJudge);
+ AddAssert("Tracking retained", assertMaxJudge);
}
///
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_1 },
});
- AddAssert("Tracking retained", assertGreatJudge);
+ AddAssert("Tracking retained", assertMaxJudge);
}
///
@@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_1 },
});
- AddAssert("Tracking retained", assertGreatJudge);
+ AddAssert("Tracking retained", assertMaxJudge);
}
///
@@ -288,7 +288,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(slider_path_length, OsuHitObject.OBJECT_RADIUS * 1.199f), Actions = { OsuAction.LeftButton }, Time = time_slider_end },
});
- AddAssert("Tracking kept", assertGreatJudge);
+ AddAssert("Tracking kept", assertMaxJudge);
}
///
@@ -312,13 +312,13 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("Tracking dropped", assertMidSliderJudgementFail);
}
- private bool assertGreatJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == HitResult.Great);
+ private bool assertMaxJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == t.Judgement.MaxResult);
- private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.Great && judgementResults.First().Type == HitResult.Miss;
+ private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.SmallTickHit && !judgementResults.First().IsHit;
- private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.Great;
+ private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.SmallTickHit;
- private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.Miss;
+ private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.SmallTickMiss;
private ScoreAccessibleReplayPlayer currentPlayer;
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
index a69646507a..3d100e4b1c 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
@@ -22,7 +22,6 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Storyboards;
using osuTK;
-using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Rulesets.Osu.Tests
{
@@ -32,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved]
private AudioManager audioManager { get; set; }
- private TrackVirtualManual track;
-
protected override bool Autoplay => autoplay;
private bool autoplay;
@@ -44,11 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private const double fade_in_modifier = -1200;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
- {
- var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
- track = (TrackVirtualManual)working.Track;
- return working;
- }
+ => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[BackgroundDependencyLoader]
private void load(RulesetConfigCache configCache)
@@ -72,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
AddStep("enable autoplay", () => autoplay = true);
base.SetUpSteps();
- AddUntilStep("wait for track to start running", () => track.IsRunning);
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
double startTime = hitObjects[sliderIndex].StartTime;
retrieveDrawableSlider(sliderIndex);
@@ -97,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
AddStep("have autoplay", () => autoplay = true);
base.SetUpSteps();
- AddUntilStep("wait for track to start running", () => track.IsRunning);
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
double startTime = hitObjects[sliderIndex].StartTime;
retrieveDrawableSlider(sliderIndex);
@@ -201,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void addSeekStep(double time)
{
- AddStep($"seek to {time}", () => track.Seek(time));
+ AddStep($"seek to {time}", () => MusicController.SeekTo(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
index 47b3926ceb..94d1cb8864 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
@@ -9,6 +9,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
@@ -62,7 +63,8 @@ namespace osu.Game.Rulesets.Osu.Tests
drawableSpinner = new TestDrawableSpinner(spinner, auto)
{
Anchor = Anchor.Centre,
- Depth = depthIndex++
+ Depth = depthIndex++,
+ Scale = new Vector2(0.75f)
};
foreach (var mod in SelectedMods.Value.OfType())
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index b46964e8b7..53bf1ea566 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -24,7 +24,6 @@ using osu.Game.Scoring;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK;
-using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Rulesets.Osu.Tests
{
@@ -33,18 +32,12 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved]
private AudioManager audioManager { get; set; }
- private TrackVirtualManual track;
-
protected override bool Autoplay => true;
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
- {
- var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
- track = (TrackVirtualManual)working.Track;
- return working;
- }
+ => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
private DrawableSpinner drawableSpinner;
private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType().Single();
@@ -54,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
base.SetUpSteps();
- AddUntilStep("wait for track to start running", () => track.IsRunning);
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First());
}
@@ -69,11 +62,11 @@ namespace osu.Game.Rulesets.Osu.Tests
trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f);
});
AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100));
- AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100));
+ AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, 0, 100));
addSeekStep(0);
AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance));
- AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100));
+ AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, 0, 100));
}
[Test]
@@ -94,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Tests
finalSpinnerSymbolRotation = spinnerSymbol.Rotation;
spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f);
});
- AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.RotationTracker.CumulativeRotation);
+ AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.RotationTracker.RateAdjustedRotation);
addSeekStep(2500);
AddAssert("disc rotation rewound",
@@ -106,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Tests
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation rewound",
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
- () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalCumulativeTrackerRotation / 2, 100));
+ () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
addSeekStep(5000);
AddAssert("is disc rotation almost same",
@@ -114,26 +107,14 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("is symbol rotation almost same",
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation almost same",
- () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalCumulativeTrackerRotation, 100));
+ () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, finalCumulativeTrackerRotation, 100));
}
[Test]
public void TestRotationDirection([Values(true, false)] bool clockwise)
{
if (clockwise)
- {
- AddStep("flip replay", () =>
- {
- var drawableRuleset = this.ChildrenOfType().Single();
- var score = drawableRuleset.ReplayScore;
- var scoreWithFlippedReplay = new Score
- {
- ScoreInfo = score.ScoreInfo,
- Replay = flipReplay(score.Replay)
- };
- drawableRuleset.SetReplayScore(scoreWithFlippedReplay);
- });
- }
+ transformReplay(flip);
addSeekStep(5000);
@@ -141,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0);
}
- private Replay flipReplay(Replay scoreReplay) => new Replay
+ private Replay flip(Replay scoreReplay) => new Replay
{
Frames = scoreReplay
.Frames
@@ -164,7 +145,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
- return totalScore == (int)(drawableSpinner.RotationTracker.CumulativeRotation / 360) * SpinnerTick.SCORE_PER_TICK;
+ return totalScore == (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult;
});
addSeekStep(0);
@@ -196,13 +177,62 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
}
+ [TestCase(0.5)]
+ [TestCase(2.0)]
+ public void TestSpinUnaffectedByClockRate(double rate)
+ {
+ double expectedProgress = 0;
+ double expectedSpm = 0;
+
+ addSeekStep(1000);
+ AddStep("retrieve spinner state", () =>
+ {
+ expectedProgress = drawableSpinner.Progress;
+ expectedSpm = drawableSpinner.SpmCounter.SpinsPerMinute;
+ });
+
+ addSeekStep(0);
+
+ AddStep("adjust track rate", () => Player.GameplayClockContainer.UserPlaybackRate.Value = rate);
+
+ addSeekStep(1000);
+ AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
+ AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpmCounter.SpinsPerMinute, 2.0));
+ }
+
+ private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
+ {
+ Frames = scoreReplay
+ .Frames
+ .Cast()
+ .Select(replayFrame =>
+ {
+ var adjustedTime = replayFrame.Time * rate;
+ return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray());
+ })
+ .Cast()
+ .ToList()
+ };
+
private void addSeekStep(double time)
{
- AddStep($"seek to {time}", () => track.Seek(time));
+ AddStep($"seek to {time}", () => MusicController.SeekTo(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
+ private void transformReplay(Func replayTransformation) => AddStep("set replay", () =>
+ {
+ var drawableRuleset = this.ChildrenOfType().Single();
+ var score = drawableRuleset.ReplayScore;
+ var transformedScore = new Score
+ {
+ ScoreInfo = score.ScoreInfo,
+ Replay = replayTransformation.Invoke(score.Replay)
+ };
+ drawableRuleset.SetReplayScore(transformedScore);
+ });
+
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
{
HitObjects = new List
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs
deleted file mode 100644
index d1210db6b1..0000000000
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.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 System.Linq;
-using NUnit.Framework;
-using osu.Framework.Graphics;
-using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Osu.Mods;
-using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osu.Game.Tests.Visual;
-
-namespace osu.Game.Rulesets.Osu.Tests
-{
- [TestFixture]
- public class TestSceneSpinnerSpunOut : OsuTestScene
- {
- [SetUp]
- public void SetUp() => Schedule(() =>
- {
- SelectedMods.Value = new[] { new OsuModSpunOut() };
- });
-
- [Test]
- public void TestSpunOut()
- {
- DrawableSpinner spinner = null;
-
- AddStep("create spinner", () => spinner = createSpinner());
-
- AddUntilStep("wait for end", () => Time.Current > spinner.LifetimeEnd);
-
- AddAssert("spinner is completed", () => spinner.Progress >= 1);
- }
-
- private DrawableSpinner createSpinner()
- {
- var spinner = new Spinner
- {
- StartTime = Time.Current + 500,
- EndTime = Time.Current + 2500
- };
- spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
-
- var drawableSpinner = new DrawableSpinner(spinner)
- {
- Anchor = Anchor.Centre
- };
-
- foreach (var mod in SelectedMods.Value.OfType())
- mod.ApplyToDrawableHitObjects(new[] { drawableSpinner });
-
- Add(drawableSpinner);
- return drawableSpinner;
- }
- }
-}
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index d6a68abaf2..3639c3616f 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs
index 491d82b89e..2d3cc3c103 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
@@ -23,19 +22,19 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
Name = @"Circle Count",
Content = circles.ToString(),
- Icon = FontAwesome.Regular.Circle
+ CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
},
new BeatmapStatistic
{
Name = @"Slider Count",
Content = sliders.ToString(),
- Icon = FontAwesome.Regular.Circle
+ CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
},
new BeatmapStatistic
{
Name = @"Spinner Count",
Content = spinners.ToString(),
- Icon = FontAwesome.Regular.Circle
+ CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
}
};
}
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
index fcad356a1c..a2fc4848af 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
@@ -8,6 +8,7 @@ using osu.Game.Rulesets.Osu.Objects;
using System.Collections.Generic;
using osu.Game.Rulesets.Objects.Types;
using System.Linq;
+using System.Threading;
using osu.Game.Rulesets.Osu.UI;
using osu.Framework.Extensions.IEnumerableExtensions;
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasPosition);
- protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap)
+ protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
{
var positionData = original as IHasPosition;
var comboData = original as IHasCombo;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
index 6e991a1d08..fff033357d 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
@@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public double SpeedStrain;
public double ApproachRate;
public double OverallDifficulty;
- public int MaxCombo;
+ public int HitCircleCount;
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index b0d261a1cc..6027635b75 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -47,6 +47,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
maxCombo += beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1);
+ int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
+
return new OsuDifficultyAttributes
{
StarRating = starRating,
@@ -56,6 +58,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6,
MaxCombo = maxCombo,
+ HitCircleCount = hitCirclesCount,
Skills = skills
};
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 6f4c0f9cfa..063cde8747 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -5,11 +5,9 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
-using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
-using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@@ -19,26 +17,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
public new OsuDifficultyAttributes Attributes => (OsuDifficultyAttributes)base.Attributes;
- private readonly int countHitCircles;
- private readonly int beatmapMaxCombo;
-
private Mod[] mods;
private double accuracy;
private int scoreMaxCombo;
private int countGreat;
- private int countGood;
+ private int countOk;
private int countMeh;
private int countMiss;
- public OsuPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score)
- : base(ruleset, beatmap, score)
+ public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
+ : base(ruleset, attributes, score)
{
- countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle);
-
- beatmapMaxCombo = Beatmap.HitObjects.Count;
- // Add the ticks + tail of the slider. 1 is subtracted because the "headcircle" would be counted twice (once for the slider itself in the line above)
- beatmapMaxCombo += Beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1);
}
public override double Calculate(Dictionary categoryRatings = null)
@@ -47,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
accuracy = Score.Accuracy;
scoreMaxCombo = Score.MaxCombo;
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);
@@ -81,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
categoryRatings.Add("Accuracy", accuracyValue);
categoryRatings.Add("OD", Attributes.OverallDifficulty);
categoryRatings.Add("AR", Attributes.ApproachRate);
- categoryRatings.Add("Max Combo", beatmapMaxCombo);
+ categoryRatings.Add("Max Combo", Attributes.MaxCombo);
}
return totalValue;
@@ -106,8 +96,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= Math.Pow(0.97, countMiss);
// Combo scaling
- if (beatmapMaxCombo > 0)
- aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0);
+ if (Attributes.MaxCombo > 0)
+ aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
double approachRateFactor = 1.0;
@@ -154,8 +144,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= Math.Pow(0.97, countMiss);
// Combo scaling
- if (beatmapMaxCombo > 0)
- speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0);
+ if (Attributes.MaxCombo > 0)
+ speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
double approachRateFactor = 1.0;
if (Attributes.ApproachRate > 10.33)
@@ -178,10 +168,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window
double betterAccuracyPercentage;
- int amountHitObjectsWithAccuracy = countHitCircles;
+ int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;
if (amountHitObjectsWithAccuracy > 0)
- betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countGood * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
+ betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
else
betterAccuracyPercentage = 0;
@@ -204,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return accuracyValue;
}
- private int totalHits => countGreat + countGood + countMeh + countMiss;
- private int totalSuccessfulHits => countGreat + countGood + countMeh;
+ private int totalHits => countGreat + countOk + countMeh + countMiss;
+ private int totalSuccessfulHits => countGreat + countOk + countMeh;
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
index 78f4c4d992..9349ef7a18 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
@@ -15,6 +15,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
private readonly ManualSliderBody body;
+ ///
+ /// Offset in absolute (local) coordinates from the start of the curve.
+ ///
+ public Vector2 PathStartLocation => body.PathOffset;
+
public SliderBodyPiece()
{
InternalChild = body = new ManualSliderBody
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 6633136673..94862eb205 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)),
};
- public override Vector2 ScreenSpaceSelectionPoint => ((DrawableSlider)DrawableObject).HeadCircle.ScreenSpaceDrawQuad.Centre;
+ public override Vector2 ScreenSpaceSelectionPoint => BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos);
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs
index 65c8720031..2347d8a34c 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs
@@ -34,11 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components
Alpha = 0.5f,
Child = new Box { RelativeSizeAxes = Axes.Both }
},
- ring = new RingPiece
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre
- }
+ ring = new RingPiece()
};
}
diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs
index a8719e0aa8..746ff4ac19 100644
--- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs
+++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs
@@ -8,6 +8,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osuTK;
@@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Edit
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
///
- private const double editor_hit_object_fade_out_extension = 500;
+ private const double editor_hit_object_fade_out_extension = 700;
public DrawableOsuEditRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods)
: base(ruleset, beatmap, mods)
@@ -32,20 +33,37 @@ namespace osu.Game.Rulesets.Osu.Edit
private void updateState(DrawableHitObject hitObject, ArmedState state)
{
- switch (state)
+ if (state == ArmedState.Idle)
+ return;
+
+ // adjust the visuals of certain object types to make them stay on screen for longer than usual.
+ switch (hitObject)
{
- case ArmedState.Miss:
- // Get the existing fade out transform
- var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
- if (existing == null)
- return;
+ default:
+ // there are quite a few drawable hit types we don't want to extent (spinners, ticks etc.)
+ return;
- hitObject.RemoveTransform(existing);
+ case DrawableSlider _:
+ // no specifics to sliders but let them fade slower below.
+ break;
- using (hitObject.BeginAbsoluteSequence(existing.StartTime))
- hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
+ case DrawableHitCircle circle: // also handles slider heads
+ circle.ApproachCircle
+ .FadeOutFromOne(editor_hit_object_fade_out_extension)
+ .Expire();
break;
}
+
+ // Get the existing fade out transform
+ var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
+
+ if (existing == null)
+ return;
+
+ hitObject.RemoveTransform(existing);
+
+ using (hitObject.BeginAbsoluteSequence(existing.StartTime))
+ hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
}
protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor();
diff --git a/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs
index 9c94fe0e3d..5f7c8b77b0 100644
--- a/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs
+++ b/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
@@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
}
+ public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
+
public override PlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint();
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 37019a7a05..912a705d16 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -5,10 +5,13 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
@@ -16,6 +19,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@@ -38,13 +42,31 @@ namespace osu.Game.Rulesets.Osu.Edit
new SpinnerCompositionTool()
};
+ private readonly Bindable distanceSnapToggle = new Bindable();
+
+ protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
+ {
+ new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
+ });
+
+ private BindableList selectedHitObjects;
+
+ private Bindable placementObject;
+
[BackgroundDependencyLoader]
private void load()
{
LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both });
- EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
- EditorBeatmap.PlacementObject.ValueChanged += _ => updateDistanceSnapGrid();
+ selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
+ selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
+
+ placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
+ placementObject.ValueChanged += _ => updateDistanceSnapGrid();
+ distanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid();
+
+ // we may be entering the screen with a selection already active
+ updateDistanceSnapGrid();
}
protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects)
@@ -75,6 +97,10 @@ namespace osu.Game.Rulesets.Osu.Edit
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{
+ if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
+ return snapResult;
+
+ // will be null if distance snap is disabled or not feasible for the current time value.
if (distanceSnapGrid == null)
return base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
@@ -83,10 +109,58 @@ namespace osu.Game.Rulesets.Osu.Edit
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
}
+ private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
+ {
+ // check other on-screen objects for snapping/stacking
+ var blueprints = BlueprintContainer.SelectionBlueprints.AliveChildren;
+
+ var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
+
+ float snapRadius =
+ playfield.GamefieldToScreenSpace(new Vector2(OsuHitObject.OBJECT_RADIUS / 5)).X -
+ playfield.GamefieldToScreenSpace(Vector2.Zero).X;
+
+ foreach (var b in blueprints)
+ {
+ if (b.IsSelected)
+ continue;
+
+ var hitObject = (OsuHitObject)b.HitObject;
+
+ Vector2? snap = checkSnap(hitObject.Position);
+ if (snap == null && hitObject.Position != hitObject.EndPosition)
+ snap = checkSnap(hitObject.EndPosition);
+
+ if (snap != null)
+ {
+ // only return distance portion, since time is not really valid
+ snapResult = new SnapResult(snap.Value, null, playfield);
+ return true;
+ }
+
+ Vector2? checkSnap(Vector2 checkPos)
+ {
+ Vector2 checkScreenPos = playfield.GamefieldToScreenSpace(checkPos);
+
+ if (Vector2.Distance(checkScreenPos, screenSpacePosition) < snapRadius)
+ return checkScreenPos;
+
+ return null;
+ }
+ }
+
+ snapResult = null;
+ return false;
+ }
+
private void updateDistanceSnapGrid()
{
distanceSnapGridContainer.Clear();
distanceSnapGridCache.Invalidate();
+ distanceSnapGrid = null;
+
+ if (distanceSnapToggle.Value != TernaryState.True)
+ return;
switch (BlueprintContainer.CurrentTool)
{
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
index 9418565907..7ae0730e39 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
@@ -1,7 +1,13 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@@ -10,40 +16,219 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuSelectionHandler : SelectionHandler
{
- public override bool HandleMovement(MoveSelectionEvent moveEvent)
+ protected override void OnSelectionChanged()
{
- Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
- Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
+ base.OnSelectionChanged();
- // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
- foreach (var h in SelectedHitObjects.OfType())
+ bool canOperate = SelectedHitObjects.Count() > 1 || SelectedHitObjects.Any(s => s is Slider);
+
+ SelectionBox.CanRotate = canOperate;
+ SelectionBox.CanScaleX = canOperate;
+ SelectionBox.CanScaleY = canOperate;
+ }
+
+ protected override void OnOperationEnded()
+ {
+ base.OnOperationEnded();
+ referenceOrigin = null;
+ }
+
+ public override bool HandleMovement(MoveSelectionEvent moveEvent) =>
+ moveSelection(moveEvent.InstantDelta);
+
+ ///
+ /// During a transform, the initial origin is stored so it can be used throughout the operation.
+ ///
+ private Vector2? referenceOrigin;
+
+ public override bool HandleFlip(Direction direction)
+ {
+ var hitObjects = selectedMovableObjects;
+
+ var selectedObjectsQuad = getSurroundingQuad(hitObjects);
+ var centre = selectedObjectsQuad.Centre;
+
+ foreach (var h in hitObjects)
{
- if (h is Spinner)
+ var pos = h.Position;
+
+ switch (direction)
{
- // Spinners don't support position adjustments
- continue;
+ case Direction.Horizontal:
+ pos.X = centre.X - (pos.X - centre.X);
+ break;
+
+ case Direction.Vertical:
+ pos.Y = centre.Y - (pos.Y - centre.Y);
+ break;
}
- // Stacking is not considered
- minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
- maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
- }
+ h.Position = pos;
- if (minPosition.X < 0 || minPosition.Y < 0 || maxPosition.X > DrawWidth || maxPosition.Y > DrawHeight)
- return false;
-
- foreach (var h in SelectedHitObjects.OfType())
- {
- if (h is Spinner)
+ if (h is Slider slider)
{
- // Spinners don't support position adjustments
- continue;
+ foreach (var point in slider.Path.ControlPoints)
+ {
+ point.Position.Value = new Vector2(
+ (direction == Direction.Horizontal ? -1 : 1) * point.Position.Value.X,
+ (direction == Direction.Vertical ? -1 : 1) * point.Position.Value.Y
+ );
+ }
}
-
- h.Position += moveEvent.InstantDelta;
}
return true;
}
+
+ public override bool HandleScale(Vector2 scale, Anchor reference)
+ {
+ adjustScaleFromAnchor(ref scale, reference);
+
+ var hitObjects = selectedMovableObjects;
+
+ // for the time being, allow resizing of slider paths only if the slider is
+ // the only hit object selected. with a group selection, it's likely the user
+ // is not looking to change the duration of the slider but expand the whole pattern.
+ if (hitObjects.Length == 1 && hitObjects.First() is Slider slider)
+ {
+ Quad quad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
+ Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / quad.Width, 1 + scale.Y / quad.Height);
+
+ foreach (var point in slider.Path.ControlPoints)
+ point.Position.Value *= pathRelativeDeltaScale;
+ }
+ else
+ {
+ // move the selection before scaling if dragging from top or left anchors.
+ if ((reference & Anchor.x0) > 0 && !moveSelection(new Vector2(-scale.X, 0))) return false;
+ if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false;
+
+ Quad quad = getSurroundingQuad(hitObjects);
+
+ foreach (var h in hitObjects)
+ {
+ h.Position = new Vector2(
+ quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X),
+ quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y)
+ );
+ }
+ }
+
+ return true;
+ }
+
+ private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
+ {
+ // cancel out scale in axes we don't care about (based on which drag handle was used).
+ if ((reference & Anchor.x1) > 0) scale.X = 0;
+ if ((reference & Anchor.y1) > 0) scale.Y = 0;
+
+ // reverse the scale direction if dragging from top or left.
+ if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
+ if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
+ }
+
+ public override bool HandleRotation(float delta)
+ {
+ var hitObjects = selectedMovableObjects;
+
+ Quad quad = getSurroundingQuad(hitObjects);
+
+ referenceOrigin ??= quad.Centre;
+
+ foreach (var h in hitObjects)
+ {
+ h.Position = rotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
+
+ if (h is IHasPath path)
+ {
+ foreach (var point in path.Path.ControlPoints)
+ point.Position.Value = rotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta);
+ }
+ }
+
+ // this isn't always the case but let's be lenient for now.
+ return true;
+ }
+
+ private bool moveSelection(Vector2 delta)
+ {
+ var hitObjects = selectedMovableObjects;
+
+ Quad quad = getSurroundingQuad(hitObjects);
+
+ if (quad.TopLeft.X + delta.X < 0 ||
+ quad.TopLeft.Y + delta.Y < 0 ||
+ quad.BottomRight.X + delta.X > DrawWidth ||
+ quad.BottomRight.Y + delta.Y > DrawHeight)
+ return false;
+
+ foreach (var h in hitObjects)
+ h.Position += delta;
+
+ return true;
+ }
+
+ ///
+ /// Returns a gamefield-space quad surrounding the provided hit objects.
+ ///
+ /// The hit objects to calculate a quad for.
+ private Quad getSurroundingQuad(OsuHitObject[] hitObjects) =>
+ getSurroundingQuad(hitObjects.SelectMany(h => new[] { h.Position, h.EndPosition }));
+
+ ///
+ /// Returns a gamefield-space quad surrounding the provided points.
+ ///
+ /// The points to calculate a quad for.
+ private Quad getSurroundingQuad(IEnumerable points)
+ {
+ if (!SelectedHitObjects.Any())
+ return new Quad();
+
+ Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
+ Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
+
+ // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
+ foreach (var p in points)
+ {
+ minPosition = Vector2.ComponentMin(minPosition, p);
+ maxPosition = Vector2.ComponentMax(maxPosition, p);
+ }
+
+ Vector2 size = maxPosition - minPosition;
+
+ return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
+ }
+
+ ///
+ /// All osu! hitobjects which can be moved/rotated/scaled.
+ ///
+ private OsuHitObject[] selectedMovableObjects => SelectedHitObjects
+ .OfType()
+ .Where(h => !(h is Spinner))
+ .ToArray();
+
+ ///
+ /// Rotate a point around an arbitrary origin.
+ ///
+ /// The point.
+ /// The centre origin to rotate around.
+ /// The angle to rotate (in degrees).
+ private static Vector2 rotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle)
+ {
+ angle = -angle;
+
+ point.X -= origin.X;
+ point.Y -= origin.Y;
+
+ Vector2 ret;
+ ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
+ ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
+
+ ret.X += origin.X;
+ ret.Y += origin.Y;
+
+ return ret;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs
index a377deb35f..596224e5c6 100644
--- a/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs
+++ b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
@@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
}
+ public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
+
public override PlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint();
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs
index 0de0af8f8c..c5e90da3bd 100644
--- a/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs
+++ b/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners;
@@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
}
+ public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
+
public override PlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint();
}
}
diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs
index e528f65dca..1999785efe 100644
--- a/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs
@@ -7,10 +7,6 @@ namespace osu.Game.Rulesets.Osu.Judgements
{
public class OsuIgnoreJudgement : OsuJudgement
{
- public override bool AffectsCombo => false;
-
- protected override int NumericResultFor(HitResult result) => 0;
-
- protected override double HealthIncreaseFor(HitResult result) => 0;
+ public override HitResult MaxResult => HitResult.IgnoreHit;
}
}
diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs
index bf30fbc351..1a88e2a8b2 100644
--- a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs
@@ -9,23 +9,5 @@ namespace osu.Game.Rulesets.Osu.Judgements
public class OsuJudgement : Judgement
{
public override HitResult MaxResult => HitResult.Great;
-
- protected override int NumericResultFor(HitResult result)
- {
- switch (result)
- {
- default:
- return 0;
-
- case HitResult.Meh:
- return 50;
-
- case HitResult.Good:
- return 100;
-
- case HitResult.Great:
- return 300;
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index 08fd13915d..80e40af717 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -39,6 +39,9 @@ namespace osu.Game.Rulesets.Osu.Mods
base.ApplyToDrawableHitObjects(drawables);
}
+ private double lastSliderHeadFadeOutStartTime;
+ private double lastSliderHeadFadeOutDuration;
+
protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state)
{
if (!(drawable is DrawableOsuHitObject d))
@@ -54,7 +57,35 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (drawable)
{
+ case DrawableSliderTail sliderTail:
+ // use stored values from head circle to achieve same fade sequence.
+ fadeOutDuration = lastSliderHeadFadeOutDuration;
+ fadeOutStartTime = lastSliderHeadFadeOutStartTime;
+
+ using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
+ sliderTail.FadeOut(fadeOutDuration);
+
+ break;
+
+ case DrawableSliderRepeat sliderRepeat:
+ // use stored values from head circle to achieve same fade sequence.
+ fadeOutDuration = lastSliderHeadFadeOutDuration;
+ fadeOutStartTime = lastSliderHeadFadeOutStartTime;
+
+ using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
+ // only apply to circle piece – reverse arrow is not affected by hidden.
+ sliderRepeat.CirclePiece.FadeOut(fadeOutDuration);
+
+ break;
+
case DrawableHitCircle circle:
+
+ if (circle is DrawableSliderHead)
+ {
+ lastSliderHeadFadeOutDuration = fadeOutDuration;
+ lastSliderHeadFadeOutStartTime = fadeOutStartTime;
+ }
+
// we don't want to see the approach circle
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
circle.ApproachCircle.Hide();
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
index 47d765fecd..f080e11933 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
@@ -41,7 +41,16 @@ namespace osu.Game.Rulesets.Osu.Mods
var spinner = (DrawableSpinner)drawable;
spinner.RotationTracker.Tracking = true;
- spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f));
+
+ // early-return if we were paused to avoid division-by-zero in the subsequent calculations.
+ if (Precision.AlmostEquals(spinner.Clock.Rate, 0))
+ return;
+
+ // because the spinner is under the gameplay clock, it is affected by rate adjustments on the track;
+ // for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time.
+ // for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here.
+ var rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate;
+ spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * 0.03f));
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
index 4d73e711bb..11571ea761 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
@@ -46,7 +46,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
private void addConnection(FollowPointConnection connection)
{
// Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections
- int index = connections.AddInPlace(connection, Comparer.Create((g1, g2) => g1.StartTime.Value.CompareTo(g2.StartTime.Value)));
+ int index = connections.AddInPlace(connection, Comparer.Create((g1, g2) =>
+ {
+ int comp = g1.StartTime.Value.CompareTo(g2.StartTime.Value);
+
+ if (comp != 0)
+ return comp;
+
+ // we always want to insert the new item after equal ones.
+ // this is important for beatmaps with multiple hitobjects at the same point in time.
+ // if we use standard comparison insert order, there will be a churn of connections getting re-updated to
+ // the next object at the point-in-time, adding a construction/disposal overhead (see FollowPointConnection.End implementation's ClearInternal).
+ // this is easily visible on https://osu.ppy.sh/beatmapsets/150945#osu/372245
+ return -1;
+ }));
if (index < connections.Count - 1)
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index a438dc8be4..b5ac26c824 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!userTriggered)
{
if (!HitObject.HitWindows.CanBeHit(timeOffset))
- ApplyResult(r => r.Type = HitResult.Miss);
+ ApplyResult(r => r.Type = r.Judgement.MinResult);
return;
}
@@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var circleResult = (OsuHitCircleJudgementResult)r;
// Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss.
- if (result != HitResult.Miss)
+ if (result.IsHit())
{
var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position);
circleResult.CursorPositionAtHit = HitObject.StackedPosition + (localMousePosition - DrawSize / 2);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index 8308c0c576..45c664ba3b 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -8,7 +8,6 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Osu.UI;
-using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -22,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override float SamplePlaybackPosition => HitObject.X / OsuPlayfield.BASE_SIZE.X;
///
- /// Whether this can be hit.
+ /// Whether this can be hit, given a time value.
/// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false.
///
public Func CheckHittable;
@@ -68,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
///
/// Causes this to get missed, disregarding all conditions in implementations of .
///
- public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss);
+ public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement);
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
index 012d9f8878..49535e7fff 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
@@ -8,7 +8,6 @@ using osu.Game.Configuration;
using osuTK;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK.Graphics;
@@ -67,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (JudgedObject != null)
{
lightingColour = JudgedObject.AccentColour.GetBoundCopy();
- lightingColour.BindValueChanged(colour => Lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true);
+ lightingColour.BindValueChanged(colour => Lighting.Colour = Result.IsHit ? colour.NewValue : Color4.Transparent, true);
}
else
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 07f40f763b..b00d12983d 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI;
-using osu.Game.Rulesets.Scoring;
using osuTK.Graphics;
using osu.Game.Skinning;
@@ -52,6 +51,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
InternalChildren = new Drawable[]
{
Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
+ tailContainer = new Container { RelativeSizeAxes = Axes.Both },
tickContainer = new Container { RelativeSizeAxes = Axes.Both },
repeatContainer = new Container { RelativeSizeAxes = Axes.Both },
Ball = new SliderBall(s, this)
@@ -63,7 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Alpha = 0
},
headContainer = new Container { RelativeSizeAxes = Axes.Both },
- tailContainer = new Container { RelativeSizeAxes = Axes.Both },
};
}
@@ -87,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Tracking.BindValueChanged(updateSlidingSample);
}
- private SkinnableSound slidingSample;
+ private PausableSkinnableSound slidingSample;
protected override void LoadSamples()
{
@@ -103,19 +102,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
clone.Name = "sliderslide";
- AddInternal(slidingSample = new SkinnableSound(clone)
+ AddInternal(slidingSample = new PausableSkinnableSound(clone)
{
Looping = true
});
}
}
+ public override void StopAllSamples()
+ {
+ base.StopAllSamples();
+ slidingSample?.Stop();
+ }
+
private void updateSlidingSample(ValueChangedEvent tracking)
{
- // note that samples will not start playing if exiting a seek operation in the middle of a slider.
- // may be something we want to address at a later point, but not so easy to make happen right now
- // (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update).
- if (tracking.NewValue && ShouldPlaySamples)
+ if (tracking.NewValue)
slidingSample?.Play();
else
slidingSample?.Stop();
@@ -247,7 +249,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
// rather than doing it this way, we should probably attach the sample to the tail circle.
// this can only be done after we stop using LegacyLastTick.
- if (TailCircle.Result.Type != HitResult.Miss)
+ if (TailCircle.IsHit)
base.PlaySamples();
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
index d79ecb7b4e..2a88f11f69 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
@@ -6,10 +6,11 @@ using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
-using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
@@ -23,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly Drawable scaleContainer;
+ public readonly Drawable CirclePiece;
+
public override bool DisplayResult => false;
public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider)
@@ -35,7 +38,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
- InternalChild = scaleContainer = new ReverseArrowPiece();
+ InternalChild = scaleContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new[]
+ {
+ // no default for this; only visible in legacy skins.
+ CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()),
+ arrow = new ReverseArrowPiece(),
+ }
+ };
}
private readonly IBindable scaleBindable = new BindableFloat();
@@ -50,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (sliderRepeat.StartTime <= Time.Current)
- ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? HitResult.Great : HitResult.Miss);
+ ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
protected override void UpdateInitialTransforms()
@@ -86,6 +100,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private bool hasRotation;
+ private readonly ReverseArrowPiece arrow;
+
public void UpdateSnakingPosition(Vector2 start, Vector2 end)
{
// When the repeat is hit, the arrow should fade out on spot rather than following the slider
@@ -115,18 +131,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
- while (Math.Abs(aimRotation - Rotation) > 180)
- aimRotation += aimRotation < Rotation ? 360 : -360;
+ while (Math.Abs(aimRotation - arrow.Rotation) > 180)
+ aimRotation += aimRotation < arrow.Rotation ? 360 : -360;
if (!hasRotation)
{
- Rotation = aimRotation;
+ arrow.Rotation = aimRotation;
hasRotation = true;
}
else
{
// If we're already snaking, interpolate to smooth out sharp curves (linear sliders, mainly).
- Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Rotation, aimRotation, 0, 50, Easing.OutQuint);
+ arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
index 29a4929c1b..f5bcecccdf 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
@@ -1,16 +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 System.Diagnostics;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Scoring;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
- public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking
+ public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking
{
- private readonly Slider slider;
+ private readonly SliderTailCircle tailCircle;
///
/// The judgement text is provided by the .
@@ -19,36 +23,82 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public bool Tracking { get; set; }
- private readonly IBindable positionBindable = new Bindable();
- private readonly IBindable pathVersion = new Bindable();
+ private readonly IBindable scaleBindable = new BindableFloat();
- public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle)
- : base(hitCircle)
+ private readonly SkinnableDrawable circlePiece;
+
+ private readonly Container scaleContainer;
+
+ public DrawableSliderTail(Slider slider, SliderTailCircle tailCircle)
+ : base(tailCircle)
{
- this.slider = slider;
-
+ this.tailCircle = tailCircle;
Origin = Anchor.Centre;
- RelativeSizeAxes = Axes.Both;
- FillMode = FillMode.Fit;
+ Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
- AlwaysPresent = true;
+ InternalChildren = new Drawable[]
+ {
+ scaleContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ // no default for this; only visible in legacy skins.
+ circlePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty())
+ }
+ },
+ };
+ }
- positionBindable.BindTo(hitCircle.PositionBindable);
- pathVersion.BindTo(slider.Path.Version);
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true);
+ scaleBindable.BindTo(HitObject.ScaleBindable);
+ }
- positionBindable.BindValueChanged(_ => updatePosition());
- pathVersion.BindValueChanged(_ => updatePosition(), true);
+ protected override void UpdateInitialTransforms()
+ {
+ base.UpdateInitialTransforms();
- // TODO: This has no drawable content. Support for skins should be added.
+ circlePiece.FadeInFromZero(HitObject.TimeFadeIn);
+ }
+
+ protected override void UpdateStateTransforms(ArmedState state)
+ {
+ base.UpdateStateTransforms(state);
+
+ Debug.Assert(HitObject.HitWindows != null);
+
+ switch (state)
+ {
+ case ArmedState.Idle:
+ this.Delay(HitObject.TimePreempt).FadeOut(500);
+
+ Expire(true);
+ break;
+
+ case ArmedState.Miss:
+ this.FadeOut(100);
+ break;
+
+ case ArmedState.Hit:
+ // todo: temporary / arbitrary
+ this.Delay(800).FadeOut();
+ break;
+ }
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (!userTriggered && timeOffset >= 0)
- ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : HitResult.Miss);
+ ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
- private void updatePosition() => Position = HitObject.Position - slider.Position;
+ public void UpdateSnakingPosition(Vector2 start, Vector2 end) =>
+ Position = tailCircle.RepeatIndex % 2 == 0 ? end : start;
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
index 66eb60aa28..9b68b446a4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
@@ -10,7 +10,6 @@ using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Skinning;
using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -64,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (timeOffset >= 0)
- ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : HitResult.Miss);
+ ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
protected override void UpdateInitialTransforms()
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 7363da0de8..936bfaeb86 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -11,6 +12,7 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
+using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Skinning;
@@ -30,6 +32,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly IBindable positionBindable = new Bindable();
+ private bool spinnerFrequencyModulate;
+
public DrawableSpinner(Spinner s)
: base(s)
{
@@ -80,9 +84,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
isSpinning.BindValueChanged(updateSpinningSample);
}
- private SkinnableSound spinningSample;
-
- private const float minimum_volume = 0.0001f;
+ private PausableSkinnableSound spinningSample;
+ private const float spinning_sample_initial_frequency = 1.0f;
+ private const float spinning_sample_modulated_base_frequency = 0.5f;
protected override void LoadSamples()
{
@@ -98,30 +102,34 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
clone.Name = "spinnerspin";
- AddInternal(spinningSample = new SkinnableSound(clone)
+ AddInternal(spinningSample = new PausableSkinnableSound(clone)
{
- Volume = { Value = minimum_volume },
+ Volume = { Value = 0 },
Looping = true,
+ Frequency = { Value = spinning_sample_initial_frequency }
});
}
}
private void updateSpinningSample(ValueChangedEvent tracking)
{
- // note that samples will not start playing if exiting a seek operation in the middle of a spinner.
- // may be something we want to address at a later point, but not so easy to make happen right now
- // (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update).
- if (tracking.NewValue && ShouldPlaySamples)
+ if (tracking.NewValue)
{
spinningSample?.Play();
spinningSample?.VolumeTo(1, 200);
}
else
{
- spinningSample?.VolumeTo(minimum_volume, 200).Finally(_ => spinningSample.Stop());
+ spinningSample?.VolumeTo(0, 200).Finally(_ => spinningSample.Stop());
}
}
+ public override void StopAllSamples()
+ {
+ base.StopAllSamples();
+ spinningSample?.Stop();
+ }
+
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);
@@ -172,6 +180,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
positionBindable.BindTo(HitObject.PositionBindable);
}
+ protected override void ApplySkin(ISkinSource skin, bool allowFallback)
+ {
+ base.ApplySkin(skin, allowFallback);
+ spinnerFrequencyModulate = skin.GetConfig(OsuSkinConfiguration.SpinnerFrequencyModulate)?.Value ?? true;
+ }
+
///
/// The completion progress of this spinner from 0..1 (clamped).
///
@@ -184,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// these become implicitly hit.
return 1;
- return Math.Clamp(RotationTracker.CumulativeRotation / 360 / Spinner.SpinsRequired, 0, 1);
+ return Math.Clamp(RotationTracker.RateAdjustedRotation / 360 / Spinner.SpinsRequired, 0, 1);
}
}
@@ -198,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return;
// Trigger a miss result for remaining ticks to avoid infinite gameplay.
- foreach (var tick in ticks.Where(t => !t.IsHit))
+ foreach (var tick in ticks.Where(t => !t.Result.HasResult))
tick.TriggerResult(false);
ApplyResult(r =>
@@ -206,11 +220,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (Progress >= 1)
r.Type = HitResult.Great;
else if (Progress > .9)
- r.Type = HitResult.Good;
+ r.Type = HitResult.Ok;
else if (Progress > .75)
r.Type = HitResult.Meh;
else if (Time.Current >= Spinner.EndTime)
- r.Type = HitResult.Miss;
+ r.Type = r.Judgement.MinResult;
});
}
@@ -221,9 +235,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (HandleUserInput)
RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
- if (spinningSample != null)
- // todo: implement SpinnerFrequencyModulate
- spinningSample.Frequency.Value = 0.5f + Progress;
+ if (spinningSample != null && spinnerFrequencyModulate)
+ spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress;
}
protected override void UpdateAfterChildren()
@@ -232,7 +245,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!SpmCounter.IsPresent && RotationTracker.Tracking)
SpmCounter.FadeIn(HitObject.TimeFadeIn);
- SpmCounter.SetRotation(RotationTracker.CumulativeRotation);
+ SpmCounter.SetRotation(RotationTracker.RateAdjustedRotation);
updateBonusScore();
}
@@ -244,7 +257,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (ticks.Count == 0)
return;
- int spins = (int)(RotationTracker.CumulativeRotation / 360);
+ int spins = (int)(RotationTracker.RateAdjustedRotation / 360);
if (spins < wholeSpins)
{
@@ -255,7 +268,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
while (wholeSpins != spins)
{
- var tick = ticks.FirstOrDefault(t => !t.IsHit);
+ var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
// tick may be null if we've hit the spin limit.
if (tick != null)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
index c390b673be..e9cede1398 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Game.Rulesets.Scoring;
-
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSpinnerTick : DrawableOsuHitObject
@@ -18,6 +16,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// Apply a judgement result.
///
/// Whether this tick was reached.
- internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : HitResult.Miss);
+ internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs
index aab01f45d4..e95cdc7ee3 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs
@@ -6,6 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
+using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
@@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
[BackgroundDependencyLoader]
- private void load(TextureStore textures)
+ private void load(TextureStore textures, DrawableHitObject drawableHitObject)
{
InternalChildren = new Drawable[]
{
@@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Origin = Anchor.Centre,
Texture = textures.Get(@"Gameplay/osu/disc"),
},
- new TrianglesPiece
+ new TrianglesPiece((int)drawableHitObject.HitObject.StartTime)
{
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs
index dfb692eba9..e855317544 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs
@@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -21,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private Spinner spinner;
+ private const float initial_scale = 1.3f;
private const float idle_alpha = 0.2f;
private const float tracking_alpha = 0.4f;
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
// we are slightly bigger than our parent, to clip the top and bottom of the circle
// this should probably be revisited when scaled spinners are a thing.
- Scale = new Vector2(1.3f);
+ Scale = new Vector2(initial_scale);
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
@@ -93,7 +93,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
base.LoadComplete();
drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200));
- drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
+ drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
+
+ updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
}
protected override void Update()
@@ -116,50 +118,66 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime));
}
- const float initial_scale = 0.2f;
- float targetScale = initial_scale + (1 - initial_scale) * drawableSpinner.Progress;
+ const float initial_fill_scale = 0.2f;
+ float targetScale = initial_fill_scale + (1 - initial_fill_scale) * drawableSpinner.Progress;
fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation;
}
- private void updateStateTransforms(ValueChangedEvent state)
+ private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
- centre.ScaleTo(0);
- mainContainer.ScaleTo(0);
+ if (!(drawableHitObject is DrawableSpinner))
+ return;
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
{
- // constant ambient rotation to give the spinner "spinning" character.
- this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
-
- centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
- mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
+ this.ScaleTo(initial_scale);
+ this.RotateTo(0);
using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
{
- centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint);
- mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
+ // constant ambient rotation to give the spinner "spinning" character.
+ this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
+ }
+
+ using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset, true))
+ {
+ switch (state)
+ {
+ case ArmedState.Hit:
+ this.ScaleTo(initial_scale * 1.2f, 320, Easing.Out);
+ this.RotateTo(mainContainer.Rotation + 180, 320);
+ break;
+
+ case ArmedState.Miss:
+ this.ScaleTo(initial_scale * 0.8f, 320, Easing.In);
+ break;
+ }
+ }
+ }
+
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ {
+ centre.ScaleTo(0);
+ mainContainer.ScaleTo(0);
+
+ using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
+ {
+ centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
+ mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
+
+ using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
+ {
+ centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint);
+ mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
+ }
}
}
// transforms we have from completing the spinner will be rolled back, so reapply immediately.
- updateComplete(state.NewValue == ArmedState.Hit, 0);
-
- using (BeginDelayedSequence(spinner.Duration, true))
- {
- switch (state.NewValue)
- {
- case ArmedState.Hit:
- this.ScaleTo(Scale * 1.2f, 320, Easing.Out);
- this.RotateTo(mainContainer.Rotation + 180, 320);
- break;
-
- case ArmedState.Miss:
- this.ScaleTo(Scale * 0.8f, 320, Easing.In);
- break;
- }
- }
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ updateComplete(state == ArmedState.Hit, 0);
}
private void updateComplete(bool complete, double duration)
@@ -177,7 +195,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
get
{
- int rotations = (int)(drawableSpinner.RotationTracker.CumulativeRotation / 360);
+ int rotations = (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360);
if (wholeRotationCount == rotations) return false;
@@ -185,5 +203,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
return true;
}
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (drawableSpinner != null)
+ drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs
index 82e4383143..619fea73bc 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs
@@ -9,7 +9,7 @@ using osu.Framework.Graphics.Shapes;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
- public class RingPiece : Container
+ public class RingPiece : CircularContainer
{
public RingPiece()
{
@@ -18,21 +18,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
- InternalChild = new CircularContainer
+ Masking = true;
+ BorderThickness = 9; // roughly matches slider borders and makes stacked circles distinctly visible from each other.
+ BorderColour = Color4.White;
+
+ Child = new Box
{
- Masking = true,
- BorderThickness = 10,
- BorderColour = Color4.White,
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- new Box
- {
- AlwaysPresent = true,
- Alpha = 0,
- RelativeSizeAxes = Axes.Both
- }
- }
+ AlwaysPresent = true,
+ Alpha = 0,
+ RelativeSizeAxes = Axes.Both
};
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs
index b499d7a92b..f483bb1b26 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs
@@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
///
public class SpinnerBonusDisplay : CompositeDrawable
{
+ private static readonly int score_per_tick = new SpinnerBonusTick().CreateJudgement().MaxNumericResult;
+
private readonly OsuSpriteText bonusCounter;
public SpinnerBonusDisplay()
@@ -36,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
return;
displayedCount = count;
- bonusCounter.Text = $"{SpinnerBonusTick.SCORE_PER_TICK * count}";
+ bonusCounter.Text = $"{score_per_tick * count}";
bonusCounter.FadeOutFromOne(1500);
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
index 0cc6c842f4..05ed38d241 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
@@ -2,11 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
+using osu.Game.Screens.Play;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
@@ -31,17 +33,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
public readonly BindableBool Complete = new BindableBool();
///
- /// The total rotation performed on the spinner disc, disregarding the spin direction.
+ /// The total rotation performed on the spinner disc, disregarding the spin direction,
+ /// adjusted for the track's playback rate.
///
///
+ ///
/// This value is always non-negative and is monotonically increasing with time
/// (i.e. will only increase if time is passing forward, but can decrease during rewind).
+ ///
+ ///
+ /// The rotation from each frame is multiplied by the clock's current playback rate.
+ /// The reason this is done is to ensure that spinners give the same score and require the same number of spins
+ /// regardless of whether speed-modifying mods are applied.
+ ///
///
///
- /// If the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise,
+ /// Assuming no speed-modifying mods are active,
+ /// if the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise,
/// this property will return the value of 720 (as opposed to 0 for ).
+ /// If Double Time is active instead (with a speed multiplier of 1.5x),
+ /// in the same scenario the property will return 720 * 1.5 = 1080.
///
- public float CumulativeRotation { get; private set; }
+ public float RateAdjustedRotation { get; private set; }
///
/// Whether the spinning is spinning at a reasonable speed to be considered visually spinning.
@@ -66,6 +79,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private bool rotationTransferred;
+ [Resolved(canBeNull: true)]
+ private GameplayClock gameplayClock { get; set; }
+
protected override void Update()
{
base.Update();
@@ -113,7 +129,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
currentRotation += angle;
- CumulativeRotation += Math.Abs(angle) * Math.Sign(Clock.ElapsedFrameTime);
+ // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
+ // (see: ModTimeRamp)
+ RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.TrueGameplayRate ?? Clock.Rate));
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs
index 0e29a1dcd8..6cdb0d3df3 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs
@@ -11,7 +11,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
protected override bool CreateNewTriangles => false;
protected override float SpawnRatio => 0.5f;
- public TrianglesPiece()
+ public TrianglesPiece(int? seed = null)
+ : base(seed)
{
TriangleScale = 1.2f;
HideAlphaDiscrepancies = false;
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 705e88040f..917382eccf 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using osu.Game.Rulesets.Objects;
using System.Linq;
using System.Threading;
+using Newtonsoft.Json;
using osu.Framework.Caching;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@@ -21,6 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
+ [JsonIgnore]
public double Duration
{
get => EndTime - StartTime;
@@ -112,8 +114,11 @@ namespace osu.Game.Rulesets.Osu.Objects
///
public double TickDistanceMultiplier = 1;
- public HitCircle HeadCircle;
- public SliderTailCircle TailCircle;
+ [JsonIgnore]
+ public HitCircle HeadCircle { get; protected set; }
+
+ [JsonIgnore]
+ public SliderTailCircle TailCircle { get; protected set; }
public Slider()
{
@@ -171,6 +176,7 @@ namespace osu.Game.Rulesets.Osu.Objects
// if this is to change, we should revisit this.
AddNested(TailCircle = new SliderTailCircle(this)
{
+ RepeatIndex = e.SpanIndex,
StartTime = e.Time,
Position = EndPosition,
StackHeight = StackHeight
@@ -178,10 +184,9 @@ namespace osu.Game.Rulesets.Osu.Objects
break;
case SliderEventType.Repeat:
- AddNested(new SliderRepeat
+ AddNested(new SliderRepeat(this)
{
RepeatIndex = e.SpanIndex,
- SpanDuration = SpanDuration,
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs
deleted file mode 100644
index 151902a752..0000000000
--- a/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-namespace osu.Game.Rulesets.Osu.Objects
-{
- public class SliderCircle : HitCircle
- {
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs
new file mode 100644
index 0000000000..a6aed2c00e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs
@@ -0,0 +1,50 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Objects
+{
+ ///
+ /// A hit circle which is at the end of a slider path (either repeat or final tail).
+ ///
+ public abstract class SliderEndCircle : HitCircle
+ {
+ private readonly Slider slider;
+
+ protected SliderEndCircle(Slider slider)
+ {
+ this.slider = slider;
+ }
+
+ public int RepeatIndex { get; set; }
+
+ public double SpanDuration => slider.SpanDuration;
+
+ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
+ {
+ base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
+
+ if (RepeatIndex > 0)
+ {
+ // Repeat points after the first span should appear behind the still-visible one.
+ TimeFadeIn = 0;
+
+ // The next end circle should appear exactly after the previous circle (on the same end) is hit.
+ TimePreempt = SpanDuration * 2;
+ }
+ else
+ {
+ // taken from osu-stable
+ const float first_end_circle_preempt_adjust = 2 / 3f;
+
+ // The first end circle should fade in with the slider.
+ TimePreempt = (StartTime - slider.StartTime) + slider.TimePreempt * first_end_circle_preempt_adjust;
+ }
+ }
+
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs
index ac6c6905e4..cca86361c2 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs
@@ -1,40 +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 System;
-using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
- public class SliderRepeat : OsuHitObject
+ public class SliderRepeat : SliderEndCircle
{
- public int RepeatIndex { get; set; }
- public double SpanDuration { get; set; }
-
- protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
+ public SliderRepeat(Slider slider)
+ : base(slider)
{
- base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
-
- // Out preempt should be one span early to give the user ample warning.
- TimePreempt += SpanDuration;
-
- // We want to show the first RepeatPoint as the TimePreempt dictates but on short (and possibly fast) sliders
- // we may need to cut down this time on following RepeatPoints to only show up to two RepeatPoints at any given time.
- if (RepeatIndex > 0)
- TimePreempt = Math.Min(SpanDuration * 2, TimePreempt);
}
- protected override HitWindows CreateHitWindows() => HitWindows.Empty;
-
public override Judgement CreateJudgement() => new SliderRepeatJudgement();
public class SliderRepeatJudgement : OsuJudgement
{
- protected override int NumericResultFor(HitResult result) => result == MaxResult ? 30 : 0;
+ public override HitResult MaxResult => HitResult.LargeTickHit;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
index 1e54b576f1..f9450062f4 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.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.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements;
@@ -13,25 +12,18 @@ namespace osu.Game.Rulesets.Osu.Objects
/// Note that this should not be used for timing correctness.
/// See usage in for more information.
///
- public class SliderTailCircle : SliderCircle
+ public class SliderTailCircle : SliderEndCircle
{
- private readonly IBindable pathVersion = new Bindable();
-
public SliderTailCircle(Slider slider)
+ : base(slider)
{
- pathVersion.BindTo(slider.Path.Version);
- pathVersion.BindValueChanged(_ => Position = slider.EndPosition);
}
- protected override HitWindows CreateHitWindows() => HitWindows.Empty;
-
public override Judgement CreateJudgement() => new SliderTailJudgement();
public class SliderTailJudgement : OsuJudgement
{
- protected override int NumericResultFor(HitResult result) => 0;
-
- public override bool AffectsCombo => false;
+ public override HitResult MaxResult => HitResult.SmallTickHit;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
index 22f3f559db..a427ee1955 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public class SliderTickJudgement : OsuJudgement
{
- protected override int NumericResultFor(HitResult result) => result == MaxResult ? 10 : 0;
+ public override HitResult MaxResult => HitResult.LargeTickHit;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
index 1658a4e7c2..194aa640f9 100644
--- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Spinner.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.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
@@ -48,14 +49,16 @@ namespace osu.Game.Rulesets.Osu.Objects
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration);
}
- protected override void CreateNestedHitObjects()
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
- base.CreateNestedHitObjects();
+ base.CreateNestedHitObjects(cancellationToken);
int totalSpins = MaximumBonusSpins + SpinsRequired;
for (int i = 0; i < totalSpins; i++)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
AddNested(i < SpinsRequired
diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
index 9c4b6f774f..235dc8710a 100644
--- a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
@@ -9,8 +9,6 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public class SpinnerBonusTick : SpinnerTick
{
- public new const int SCORE_PER_TICK = 50;
-
public SpinnerBonusTick()
{
Samples.Add(new HitSampleInfo { Name = "spinnerbonus" });
@@ -20,9 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
{
- protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
-
- protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2;
+ public override HitResult MaxResult => HitResult.LargeBonus;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
index de3ae27e55..d715b9a428 100644
--- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
@@ -9,19 +9,13 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public class SpinnerTick : OsuHitObject
{
- public const int SCORE_PER_TICK = 10;
-
public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public class OsuSpinnerTickJudgement : OsuJudgement
{
- public override bool AffectsCombo => false;
-
- protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
-
- protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0;
+ public override HitResult MaxResult => HitResult.SmallBonus;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index eaa5d8937a..cc2eebdd36 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -30,14 +30,12 @@ using osu.Game.Scoring;
using osu.Game.Skinning;
using System;
using System.Linq;
-using osu.Framework.Testing;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Osu
{
- [ExcludeFromDynamicCompile]
public class OsuRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableOsuRuleset(this, beatmap, mods);
@@ -173,7 +171,7 @@ namespace osu.Game.Rulesets.Osu
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(this, beatmap);
- public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new OsuPerformanceCalculator(this, beatmap, score);
+ public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new OsuPerformanceCalculator(this, attributes, score);
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);
@@ -193,30 +191,46 @@ namespace osu.Game.Rulesets.Osu
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
- public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
+ public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
- new StatisticRow
+ var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
+
+ return new[]
{
- Columns = new[]
+ new StatisticRow
{
- new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList())
+ Columns = new[]
{
- RelativeSizeAxes = Axes.X,
- Height = 250
- }),
- }
- },
- new StatisticRow
- {
- Columns = new[]
+ new StatisticItem("Timing Distribution",
+ new HitEventTimingDistributionGraph(timedHitEvents)
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }),
+ }
+ },
+ new StatisticRow
{
- new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap)
+ Columns = new[]
{
- RelativeSizeAxes = Axes.X,
- Height = 250
- }),
+ new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap)
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }),
+ }
+ },
+ new StatisticRow
+ {
+ Columns = new[]
+ {
+ new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
+ {
+ new UnstableRate(timedHitEvents)
+ }))
+ }
}
- }
- };
+ };
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
index 5468764692..2883f0c187 100644
--- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
+++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
@@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu
ReverseArrow,
HitCircleText,
SliderHeadHitCircle,
+ SliderTailHitCircle,
SliderFollowCircle,
SliderBall,
SliderBody,
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
index 4cb2cd6539..954a217473 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
@@ -72,6 +72,9 @@ namespace osu.Game.Rulesets.Osu.Replays
public override Replay Generate()
{
+ if (Beatmap.HitObjects.Count == 0)
+ return Replay;
+
buttonIndex = 0;
AddFrameToReplay(new OsuReplayFrame(-100000, new Vector2(256, 500)));
@@ -134,13 +137,13 @@ namespace osu.Game.Rulesets.Osu.Replays
if (!(h is Spinner))
AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.WindowFor(HitResult.Meh), new Vector2(h.StackedPosition.X, h.StackedPosition.Y)));
}
- else if (h.StartTime - hitWindows.WindowFor(HitResult.Good) > endTime + hitWindows.WindowFor(HitResult.Good) + 50)
+ else if (h.StartTime - hitWindows.WindowFor(HitResult.Ok) > endTime + hitWindows.WindowFor(HitResult.Ok) + 50)
{
if (!(prev is Spinner) && h.StartTime - endTime < 1000)
- AddFrameToReplay(new OsuReplayFrame(endTime + hitWindows.WindowFor(HitResult.Good), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y)));
+ AddFrameToReplay(new OsuReplayFrame(endTime + hitWindows.WindowFor(HitResult.Ok), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y)));
if (!(h is Spinner))
- AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.WindowFor(HitResult.Good), new Vector2(h.StackedPosition.X, h.StackedPosition.Y)));
+ AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.WindowFor(HitResult.Ok), new Vector2(h.StackedPosition.X, h.StackedPosition.Y)));
}
}
@@ -154,8 +157,12 @@ namespace osu.Game.Rulesets.Osu.Replays
// The startPosition for the slider should not be its .Position, but the point on the circle whose tangent crosses the current cursor position
// We also modify spinnerDirection so it spins in the direction it enters the spin circle, to make a smooth transition.
// TODO: Shouldn't the spinner always spin in the same direction?
- if (h is Spinner)
+ if (h is Spinner spinner)
{
+ // spinners with 0 spins required will auto-complete - don't bother
+ if (spinner.SpinsRequired == 0)
+ return;
+
calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[^1]).Position, out startPosition, out spinnerDirection);
Vector2 spinCentreOffset = SPINNER_CENTRE - ((OsuReplayFrame)Frames[^1]).Position;
diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
index 6f2998006f..dafe63a6d1 100644
--- a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
+++ b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
@@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
private static readonly DifficultyRange[] osu_ranges =
{
new DifficultyRange(HitResult.Great, 80, 50, 20),
- new DifficultyRange(HitResult.Good, 140, 100, 60),
+ new DifficultyRange(HitResult.Ok, 140, 100, 60),
new DifficultyRange(HitResult.Meh, 200, 150, 100),
new DifficultyRange(HitResult.Miss, 400, 400, 400),
};
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
switch (result)
{
case HitResult.Great:
- case HitResult.Good:
+ case HitResult.Ok:
case HitResult.Meh:
case HitResult.Miss:
return true;
diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
index 86ec76e373..44118227d9 100644
--- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
+++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
@@ -25,7 +25,5 @@ namespace osu.Game.Rulesets.Osu.Scoring
return new OsuJudgementResult(hitObject, judgement);
}
}
-
- public override HitWindows CreateHitWindows() => new OsuHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs
index 1885c76fcc..e6cd7bc59d 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs
@@ -1,9 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Skinning;
@@ -15,6 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
private bool disjointTrail;
private double lastTrailTime;
+ private IBindable cursorSize;
public LegacyCursorTrail()
{
@@ -22,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
}
[BackgroundDependencyLoader]
- private void load(ISkinSource skin)
+ private void load(ISkinSource skin, OsuConfigManager config)
{
Texture = skin.GetTexture("cursortrail");
disjointTrail = skin.GetTexture("cursormiddle") == null;
@@ -32,12 +36,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
// stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation.
Texture.ScaleAdjust *= 1.6f;
}
+
+ cursorSize = config.GetBindable(OsuSetting.GameplayCursorSize).GetBoundCopy();
}
protected override double FadeDuration => disjointTrail ? 150 : 500;
protected override bool InterpolateMovements => !disjointTrail;
+ protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
+
protected override bool OnMouseMove(MouseMoveEvent e)
{
if (!disjointTrail)
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
index 0ab3e8825b..382d6e53cc 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
@@ -21,10 +21,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
public class LegacyMainCirclePiece : CompositeDrawable
{
private readonly string priorityLookup;
+ private readonly bool hasNumber;
- public LegacyMainCirclePiece(string priorityLookup = null)
+ public LegacyMainCirclePiece(string priorityLookup = null, bool hasNumber = true)
{
this.priorityLookup = priorityLookup;
+ this.hasNumber = hasNumber;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
}
@@ -47,6 +49,23 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject;
+ bool allowFallback = false;
+
+ // attempt lookup using priority specification
+ Texture baseTexture = getTextureWithFallback(string.Empty);
+
+ // if the base texture was not found without a fallback, switch on fallback mode and re-perform the lookup.
+ if (baseTexture == null)
+ {
+ allowFallback = true;
+ baseTexture = getTextureWithFallback(string.Empty);
+ }
+
+ // at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it.
+ // the flow above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
+ // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin).
+ Texture overlayTexture = getTextureWithFallback("overlay");
+
InternalChildren = new Drawable[]
{
circleSprites = new Container
@@ -58,20 +77,23 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
hitCircleSprite = new Sprite
{
- Texture = getTextureWithFallback(string.Empty),
- Colour = drawableObject.AccentColour.Value,
+ Texture = baseTexture,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
hitCircleOverlay = new Sprite
{
- Texture = getTextureWithFallback("overlay"),
+ Texture = overlayTexture,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
},
- hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
+ };
+
+ if (hasNumber)
+ {
+ AddInternal(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
{
Font = OsuFont.Numeric.With(size: 40),
UseFullGlyphHeight = false,
@@ -79,8 +101,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- },
- };
+ });
+ }
bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
@@ -96,8 +118,13 @@ namespace osu.Game.Rulesets.Osu.Skinning
Texture tex = null;
if (!string.IsNullOrEmpty(priorityLookup))
+ {
tex = skin.GetTexture($"{priorityLookup}{name}");
+ if (!allowFallback)
+ return tex;
+ }
+
return tex ?? skin.GetTexture($"hitcircle{name}");
}
}
@@ -107,8 +134,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
base.LoadComplete();
state.BindValueChanged(updateState, true);
- accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
- indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
+ accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
+ if (hasNumber)
+ indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
}
private void updateState(ValueChangedEvent state)
@@ -121,16 +149,19 @@ namespace osu.Game.Rulesets.Osu.Skinning
circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
- var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value;
-
- if (legacyVersion >= 2.0m)
- // legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
- hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
- else
+ if (hasNumber)
{
- // old skins scale and fade it normally along other pieces.
- hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
- hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value;
+
+ if (legacyVersion >= 2.0m)
+ // legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
+ hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
+ else
+ {
+ // old skins scale and fade it normally along other pieces.
+ hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
+ hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ }
}
break;
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs
index 72bc3ddc9a..56b5571ce1 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -71,20 +70,30 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
base.LoadComplete();
- this.FadeOut();
- drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
+ drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
}
- private void updateStateTransforms(ValueChangedEvent state)
+ private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
+ if (!(drawableHitObject is DrawableSpinner))
+ return;
+
var spinner = (Spinner)drawableSpinner.HitObject;
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
- this.FadeInFromZero(spinner.TimePreempt / 2);
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ this.FadeOut();
- fixedMiddle.FadeColour(Color4.White);
- using (BeginAbsoluteSequence(spinner.StartTime, true))
- fixedMiddle.FadeColour(Color4.Red, spinner.Duration);
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
+ this.FadeInFromZero(spinner.TimeFadeIn / 2);
+
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ {
+ fixedMiddle.FadeColour(Color4.White);
+
+ using (BeginDelayedSequence(spinner.TimePreempt, true))
+ fixedMiddle.FadeColour(Color4.Red, spinner.Duration);
+ }
}
protected override void Update()
@@ -95,5 +104,13 @@ namespace osu.Game.Rulesets.Osu.Skinning
Scale = new Vector2(final_scale * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, drawableSpinner.Progress) * 0.2f));
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (drawableSpinner != null)
+ drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs
index 0ae1d8f683..7b0d7acbbc 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs
@@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -22,93 +21,131 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
private DrawableSpinner drawableSpinner;
private Sprite disc;
+ private Sprite metreSprite;
private Container metre;
- private const float background_y_offset = 20;
+ private bool spinnerBlink;
private const float sprite_scale = 1 / 1.6f;
+ private const float final_metre_height = 692 * sprite_scale;
[BackgroundDependencyLoader]
private void load(ISkinSource source, DrawableHitObject drawableObject)
{
+ spinnerBlink = source.GetConfig(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true;
+
drawableSpinner = (DrawableSpinner)drawableObject;
RelativeSizeAxes = Axes.Both;
- InternalChildren = new Drawable[]
+ InternalChild = new Container
{
- new Sprite
+ // the old-style spinner relied heavily on absolute screen-space coordinate values.
+ // wrap everything in a container simulating absolute coords to preserve alignment
+ // as there are skins that depend on it.
+ Width = 640,
+ Height = 480,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
{
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- Texture = source.GetTexture("spinner-background"),
- Y = background_y_offset,
- Scale = new Vector2(sprite_scale)
- },
- disc = new Sprite
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Texture = source.GetTexture("spinner-circle"),
- Scale = new Vector2(sprite_scale)
- },
- metre = new Container
- {
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- Y = background_y_offset,
- Masking = true,
- Child = new Sprite
+ new Sprite
{
- Texture = source.GetTexture("spinner-metre"),
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-background"),
+ Scale = new Vector2(sprite_scale)
},
- Scale = new Vector2(0.625f)
+ disc = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-circle"),
+ Scale = new Vector2(sprite_scale)
+ },
+ metre = new Container
+ {
+ AutoSizeAxes = Axes.Both,
+ // this anchor makes no sense, but that's what stable uses.
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ // adjustment for stable (metre has additional offset)
+ Margin = new MarginPadding { Top = 20 },
+ Masking = true,
+ Child = metreSprite = new Sprite
+ {
+ Texture = source.GetTexture("spinner-metre"),
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Scale = new Vector2(0.625f)
+ }
+ }
}
};
}
- private Vector2 metreFinalSize;
-
protected override void LoadComplete()
{
base.LoadComplete();
- this.FadeOut();
- drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
-
- metreFinalSize = metre.Size = metre.Child.Size;
+ drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
}
- private void updateStateTransforms(ValueChangedEvent state)
+ private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
+ if (!(drawableHitObject is DrawableSpinner))
+ return;
+
var spinner = drawableSpinner.HitObject;
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
- this.FadeInFromZero(spinner.TimePreempt / 2);
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ this.FadeOut();
+
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
+ this.FadeInFromZero(spinner.TimeFadeIn / 2);
}
protected override void Update()
{
base.Update();
disc.Rotation = drawableSpinner.RotationTracker.Rotation;
- metre.Height = getMetreHeight(drawableSpinner.Progress);
+
+ // careful: need to call this exactly once for all calculations in a frame
+ // as the function has a random factor in it
+ var metreHeight = getMetreHeight(drawableSpinner.Progress);
+
+ // hack to make the metre blink up from below than down from above.
+ // move down the container to be able to apply masking for the metre,
+ // and then move the sprite back up the same amount to keep its position absolute.
+ metre.Y = final_metre_height - metreHeight;
+ metreSprite.Y = -metre.Y;
}
private const int total_bars = 10;
private float getMetreHeight(float progress)
{
- progress = Math.Min(99, progress * 100);
+ progress *= 100;
+
+ // the spinner should still blink at 100% progress.
+ if (spinnerBlink)
+ progress = Math.Min(99, progress);
int barCount = (int)progress / 10;
- // todo: add SpinnerNoBlink support
- if (RNG.NextBool(((int)progress % 10) / 10f))
+ if (spinnerBlink && RNG.NextBool(((int)progress % 10) / 10f))
barCount++;
- return (float)barCount / total_bars * metreFinalSize.Y;
+ return (float)barCount / total_bars * final_metre_height;
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (drawableSpinner != null)
+ drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs
index 0f586034d5..25ab96445a 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin, DrawableHitObject drawableObject)
{
- animationContent.Colour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White;
+ var ballColour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White;
InternalChildren = new[]
{
@@ -39,11 +39,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
Texture = skin.GetTexture("sliderb-nd"),
Colour = new Color4(5, 5, 5, 255),
},
- animationContent.With(d =>
+ LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d =>
{
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
- }),
+ }), ballColour),
layerSpec = new Sprite
{
Anchor = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs
index 21df49d80b..aad8b189d9 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs
@@ -18,6 +18,10 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS);
+ protected new float CalculatedBorderPortion
+ // Roughly matches osu!stable's slider border portions.
+ => base.CalculatedBorderPortion * 0.77f;
+
public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f);
protected override Color4 ColourAt(float position)
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
index 81d1d05b66..78bc26eff7 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
@@ -66,6 +66,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
return null;
+ case OsuSkinComponents.SliderTailHitCircle:
+ if (hasHitCircle.Value)
+ return new LegacyMainCirclePiece("sliderendcircle", false);
+
+ return null;
+
case OsuSkinComponents.SliderHeadHitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece("sliderstartcircle");
@@ -94,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
var font = GetConfig(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default";
var overlap = GetConfig(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2;
- return !hasFont(font)
+ return !this.HasFont(font)
? null
: new LegacySpriteText(Source, font)
{
@@ -145,7 +151,5 @@ namespace osu.Game.Rulesets.Osu.Skinning
return Source.GetConfig(lookup);
}
-
- private bool hasFont(string fontName) => Source.GetTexture($"{fontName}-0") != null;
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
index 154160fdb5..63c9b53278 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
@@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
CursorExpand,
CursorRotate,
HitCircleOverlayAboveNumber,
- HitCircleOverlayAboveNumer // Some old skins will have this typo
+ HitCircleOverlayAboveNumer, // Some old skins will have this typo
+ SpinnerFrequencyModulate,
+ SpinnerNoBlink
}
}
diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
index 20adbc1c02..88c855d768 100644
--- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
+++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
@@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
protected void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius)
{
- if (pointGrid.Content.Length == 0)
+ if (pointGrid.Content.Count == 0)
return;
double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point.
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
index 9bcb3abc63..0b30c28b8d 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
@@ -119,6 +119,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
///
protected virtual bool InterpolateMovements => true;
+ protected virtual float IntervalMultiplier => 1.0f;
+
private Vector2? lastPosition;
private readonly InputResampler resampler = new InputResampler();
@@ -147,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
float distance = diff.Length;
Vector2 direction = diff / distance;
- float interval = partSize.X / 2.5f;
+ float interval = partSize.X / 2.5f * IntervalMultiplier;
for (float d = interval; d < distance; d += interval)
{
diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
index 88adf72551..3870f303b4 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
@@ -27,17 +27,17 @@ namespace osu.Game.Rulesets.Osu.UI
new SettingsCheckbox
{
LabelText = "Snaking in sliders",
- Bindable = config.GetBindable(OsuRulesetSetting.SnakingInSliders)
+ Current = config.GetBindable(OsuRulesetSetting.SnakingInSliders)
},
new SettingsCheckbox
{
LabelText = "Snaking out sliders",
- Bindable = config.GetBindable(OsuRulesetSetting.SnakingOutSliders)
+ Current = config.GetBindable(OsuRulesetSetting.SnakingOutSliders)
},
new SettingsCheckbox
{
LabelText = "Cursor trail",
- Bindable = config.GetBindable(OsuRulesetSetting.ShowCursorTrail)
+ Current = config.GetBindable(OsuRulesetSetting.ShowCursorTrail)
},
};
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs
new file mode 100644
index 0000000000..d1c4a1c56d
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.UI;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+ public abstract class DrawableTaikoRulesetTestScene : OsuTestScene
+ {
+ protected DrawableTaikoRuleset DrawableRuleset { get; private set; }
+ protected Container PlayfieldContainer { get; private set; }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var controlPointInfo = new ControlPointInfo();
+ controlPointInfo.Add(0, new TimingControlPoint());
+
+ WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
+ {
+ HitObjects = new List { new Hit { Type = HitType.Centre } },
+ BeatmapInfo = new BeatmapInfo
+ {
+ BaseDifficulty = new BeatmapDifficulty(),
+ Metadata = new BeatmapMetadata
+ {
+ Artist = @"Unknown",
+ Title = @"Sample Beatmap",
+ AuthorString = @"peppy",
+ },
+ Ruleset = new TaikoRuleset().RulesetInfo
+ },
+ ControlPointInfo = controlPointInfo
+ });
+
+ Add(PlayfieldContainer = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.X,
+ Height = 768,
+ Children = new[] { DrawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(new TaikoRuleset().RulesetInfo)) }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs
index 1db07b3244..4eeb4a1475 100644
--- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs
@@ -2,26 +2,36 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Tests
{
- internal class DrawableTestHit : DrawableTaikoHitObject
+ public class DrawableTestHit : DrawableHit
{
- private readonly HitResult type;
+ public readonly HitResult Type;
public DrawableTestHit(Hit hit, HitResult type = HitResult.Great)
: base(hit)
{
- this.type = type;
+ Type = type;
+
+ HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ }
+
+ protected override void UpdateInitialTransforms()
+ {
+ // base implementation in DrawableHitObject forces alpha to 1.
+ // suppress locally to allow hiding the visuals wherever necessary.
}
[BackgroundDependencyLoader]
private void load()
{
- Result.Type = type;
+ Result.Type = Type;
}
public override bool OnPressed(TaikoAction action) => false;
diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs
new file mode 100644
index 0000000000..829bcf34a1
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+ public class DrawableTestStrongHit : DrawableTestHit
+ {
+ private readonly bool hitBoth;
+
+ public DrawableTestStrongHit(double startTime, HitResult type = HitResult.Great, bool hitBoth = true)
+ : base(new Hit
+ {
+ IsStrong = true,
+ StartTime = startTime,
+ }, type)
+ {
+ this.hitBoth = hitBoth;
+ }
+
+ protected override void LoadAsyncComplete()
+ {
+ base.LoadAsyncComplete();
+
+ var nestedStrongHit = (DrawableStrongNestedHit)NestedHitObjects.Single();
+ nestedStrongHit.Result.Type = hitBoth ? Type : HitResult.Miss;
+ }
+
+ public override bool OnPressed(TaikoAction action) => false;
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditor.cs
similarity index 88%
rename from osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs
rename to osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditor.cs
index 411fe08bcf..e3c1613bd9 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditor.cs
@@ -4,7 +4,7 @@
using NUnit.Framework;
using osu.Game.Tests.Visual;
-namespace osu.Game.Rulesets.Taiko.Tests
+namespace osu.Game.Rulesets.Taiko.Tests.Editor
{
[TestFixture]
public class TestSceneEditor : EditorTestScene
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs
similarity index 97%
rename from osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs
rename to osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs
index 34d5fdf857..626537053a 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs
@@ -12,7 +12,7 @@ using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
-namespace osu.Game.Rulesets.Taiko.Tests
+namespace osu.Game.Rulesets.Taiko.Tests.Editor
{
public class TestSceneTaikoHitObjectComposer : EditorClockTestScene
{
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
index 47d8a5c012..99e103da3b 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
createDrawableRuleset();
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
- assertStateAfterResult(new JudgementResult(new StrongHitObject(), new TaikoStrongJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Idle);
+ assertStateAfterResult(new JudgementResult(new StrongHitObject(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle);
}
[Test]
@@ -102,8 +102,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
createDrawableRuleset();
- assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Kiai);
- assertStateAfterResult(new JudgementResult(new Hit(), new TaikoStrongJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Kiai);
+ assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }, TaikoMascotAnimationState.Kiai);
+ assertStateAfterResult(new JudgementResult(new Hit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Kiai);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail);
}
@@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail);
assertStateAfterResult(new JudgementResult(new DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
- assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Idle);
+ assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }, TaikoMascotAnimationState.Idle);
}
[TestCase(true)]
@@ -130,7 +130,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
AddRepeatStep("reach 49 combo", () => applyNewResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }), 49);
- assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Clear);
+ assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }, TaikoMascotAnimationState.Clear);
}
[TestCase(true, TaikoMascotAnimationState.Kiai)]
@@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
foreach (var playfield in playfields)
{
var hit = new DrawableTestHit(new Hit(), judgementResult.Type);
- Add(hit);
+ playfield.Add(hit);
playfield.OnNewResult(hit, judgementResult);
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
index 2b5efec7f9..fecb5d4a74 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
@@ -2,12 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
-using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.UI;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
@@ -15,25 +13,32 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[TestFixture]
public class TestSceneHitExplosion : TaikoSkinnableTestScene
{
- [BackgroundDependencyLoader]
- private void load()
+ [Test]
+ public void TestNormalHit()
{
- AddStep("Great", () => SetContents(() => getContentFor(HitResult.Great)));
- AddStep("Good", () => SetContents(() => getContentFor(HitResult.Good)));
- AddStep("Miss", () => SetContents(() => getContentFor(HitResult.Miss)));
+ AddStep("Great", () => SetContents(() => getContentFor(createHit(HitResult.Great))));
+ AddStep("Ok", () => SetContents(() => getContentFor(createHit(HitResult.Ok))));
+ AddStep("Miss", () => SetContents(() => getContentFor(createHit(HitResult.Miss))));
}
- private Drawable getContentFor(HitResult type)
+ [Test]
+ public void TestStrongHit([Values(false, true)] bool hitBoth)
{
- DrawableTaikoHitObject hit;
+ AddStep("Great", () => SetContents(() => getContentFor(createStrongHit(HitResult.Great, hitBoth))));
+ AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Ok, hitBoth))));
+ }
+ private Drawable getContentFor(DrawableTestHit hit)
+ {
return new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
- hit = createHit(type),
- new HitExplosion(hit)
+ // the hit needs to be added to hierarchy in order for nested objects to be created correctly.
+ // setting zero alpha is supposed to prevent the test from looking broken.
+ hit.With(h => h.Alpha = 0),
+ new HitExplosion(hit, hit.Type)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -42,6 +47,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
};
}
- private DrawableTaikoHitObject createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type);
+ private DrawableTestHit createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type);
+
+ private DrawableTestHit createStrongHit(HitResult type, bool hitBoth) => new DrawableTestStrongHit(Time.Current, type, hitBoth);
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs
index 16ef5b968d..114038b81c 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
}));
AddToggleStep("Toggle passing", passing => this.ChildrenOfType().ForEach(s => s.LastResult.Value =
- new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Perfect : HitResult.Miss }));
+ new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Great : HitResult.Miss }));
AddToggleStep("toggle playback direction", reversed => this.reversed = reversed);
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
index d0c57b20c0..5e550a5d03 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
@@ -12,6 +12,7 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
+ [Timeout(10000)]
public class TaikoBeatmapConversionTest : BeatmapConversionTest
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index e7b6d8615b..71b3c23b50 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
- [TestCase(2.9811338051242915d, "diffcalc-test")]
- [TestCase(2.9811338051242915d, "diffcalc-test-strong")]
+ [TestCase(2.2867022617692685d, "diffcalc-test")]
+ [TestCase(2.2867022617692685d, "diffcalc-test-strong")]
public void Test(double expected, string name)
=> base.Test(expected, name);
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs
new file mode 100644
index 0000000000..63854e7ead
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.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.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Judgements;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.UI;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+ [TestFixture]
+ public class TestSceneFlyingHits : DrawableTaikoRulesetTestScene
+ {
+ [TestCase(HitType.Centre)]
+ [TestCase(HitType.Rim)]
+ public void TestFlyingHits(HitType hitType)
+ {
+ DrawableFlyingHit flyingHit = null;
+
+ AddStep("add flying hit", () =>
+ {
+ addFlyingHit(hitType);
+
+ // flying hits all land in one common scrolling container (and stay there for rewind purposes),
+ // so we need to manually get the latest one.
+ flyingHit = this.ChildrenOfType()
+ .OrderByDescending(h => h.HitObject.StartTime)
+ .FirstOrDefault();
+ });
+
+ AddAssert("hit type is correct", () => flyingHit.HitObject.Type == hitType);
+ }
+
+ private void addFlyingHit(HitType hitType)
+ {
+ var tick = new DrumRollTick { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current };
+
+ DrawableDrumRollTick h;
+ DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType });
+ ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(tick, new TaikoDrumRollTickJudgement()) { Type = HitResult.Great });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
index 44452d70c1..e4c0766844 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
@@ -2,11 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -18,13 +15,12 @@ using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.UI;
-using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
- public class TestSceneHits : OsuTestScene
+ public class TestSceneHits : DrawableTaikoRulesetTestScene
{
private const double default_duration = 3000;
private const float scroll_time = 1000;
@@ -32,11 +28,9 @@ namespace osu.Game.Rulesets.Taiko.Tests
protected override double TimePerAction => default_duration * 2;
private readonly Random rng = new Random(1337);
- private DrawableTaikoRuleset drawableRuleset;
- private Container playfieldContainer;
- [BackgroundDependencyLoader]
- private void load()
+ [Test]
+ public void TestVariousHits()
{
AddStep("Hit", () => addHitJudgement(false));
AddStep("Strong hit", () => addStrongHitJudgement(false));
@@ -64,35 +58,6 @@ namespace osu.Game.Rulesets.Taiko.Tests
AddStep("Height test 4", () => changePlayfieldSize(4));
AddStep("Height test 5", () => changePlayfieldSize(5));
AddStep("Reset height", () => changePlayfieldSize(6));
-
- var controlPointInfo = new ControlPointInfo();
- controlPointInfo.Add(0, new TimingControlPoint());
-
- WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
- {
- HitObjects = new List { new Hit { Type = HitType.Centre } },
- BeatmapInfo = new BeatmapInfo
- {
- BaseDifficulty = new BeatmapDifficulty(),
- Metadata = new BeatmapMetadata
- {
- Artist = @"Unknown",
- Title = @"Sample Beatmap",
- AuthorString = @"peppy",
- },
- Ruleset = new TaikoRuleset().RulesetInfo
- },
- ControlPointInfo = controlPointInfo
- });
-
- Add(playfieldContainer = new Container
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.X,
- Height = 768,
- Children = new[] { drawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(new TaikoRuleset().RulesetInfo)) }
- });
}
private void changePlayfieldSize(int step)
@@ -128,18 +93,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
switch (step)
{
default:
- playfieldContainer.Delay(delay).ResizeTo(new Vector2(1, rng.Next(25, 400)), 500);
+ PlayfieldContainer.Delay(delay).ResizeTo(new Vector2(1, rng.Next(25, 400)), 500);
break;
case 6:
- playfieldContainer.Delay(delay).ResizeTo(new Vector2(1, TaikoPlayfield.DEFAULT_HEIGHT), 500);
+ PlayfieldContainer.Delay(delay).ResizeTo(new Vector2(1, TaikoPlayfield.DEFAULT_HEIGHT), 500);
break;
}
}
private void addHitJudgement(bool kiai)
{
- HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great;
+ HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great;
var cpi = new ControlPointInfo();
cpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
@@ -147,16 +112,16 @@ namespace osu.Game.Rulesets.Taiko.Tests
Hit hit = new Hit();
hit.ApplyDefaults(cpi, new BeatmapDifficulty());
- var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) };
+ var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) };
- Add(h);
+ DrawableRuleset.Playfield.Add(h);
- ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
+ ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
}
private void addStrongHitJudgement(bool kiai)
{
- HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great;
+ HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great;
var cpi = new ControlPointInfo();
cpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
@@ -164,37 +129,39 @@ namespace osu.Game.Rulesets.Taiko.Tests
Hit hit = new Hit();
hit.ApplyDefaults(cpi, new BeatmapDifficulty());
- var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) };
+ var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) };
- Add(h);
+ DrawableRuleset.Playfield.Add(h);
- ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
- ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great });
+ ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
+ ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great });
}
private void addMissJudgement()
{
- ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(new DrawableTestHit(new Hit()), new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss });
+ DrawableTestHit h;
+ DrawableRuleset.Playfield.Add(h = new DrawableTestHit(new Hit(), HitResult.Miss));
+ ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss });
}
private void addBarLine(bool major, double delay = scroll_time)
{
- BarLine bl = new BarLine { StartTime = drawableRuleset.Playfield.Time.Current + delay };
+ BarLine bl = new BarLine { StartTime = DrawableRuleset.Playfield.Time.Current + delay };
- drawableRuleset.Playfield.Add(major ? new DrawableBarLineMajor(bl) : new DrawableBarLine(bl));
+ DrawableRuleset.Playfield.Add(major ? new DrawableBarLineMajor(bl) : new DrawableBarLine(bl));
}
private void addSwell(double duration = default_duration)
{
var swell = new Swell
{
- StartTime = drawableRuleset.Playfield.Time.Current + scroll_time,
+ StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
Duration = duration,
};
swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- drawableRuleset.Playfield.Add(new DrawableSwell(swell));
+ DrawableRuleset.Playfield.Add(new DrawableSwell(swell));
}
private void addDrumRoll(bool strong, double duration = default_duration, bool kiai = false)
@@ -204,7 +171,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
var d = new DrumRoll
{
- StartTime = drawableRuleset.Playfield.Time.Current + scroll_time,
+ StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong,
Duration = duration,
TickRate = 8,
@@ -215,33 +182,33 @@ namespace osu.Game.Rulesets.Taiko.Tests
d.ApplyDefaults(cpi, new BeatmapDifficulty());
- drawableRuleset.Playfield.Add(new DrawableDrumRoll(d));
+ DrawableRuleset.Playfield.Add(new DrawableDrumRoll(d));
}
private void addCentreHit(bool strong)
{
Hit h = new Hit
{
- StartTime = drawableRuleset.Playfield.Time.Current + scroll_time,
+ StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong
};
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- drawableRuleset.Playfield.Add(new DrawableHit(h));
+ DrawableRuleset.Playfield.Add(new DrawableHit(h));
}
private void addRimHit(bool strong)
{
Hit h = new Hit
{
- StartTime = drawableRuleset.Playfield.Time.Current + scroll_time,
+ StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong
};
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- drawableRuleset.Playfield.Add(new DrawableHit(h));
+ DrawableRuleset.Playfield.Add(new DrawableHit(h));
}
private class TestStrongNestedHit : DrawableStrongNestedHit
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index ada7ac5d74..b59f3a4344 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
index 7c39c040b1..fcf7c529f5 100644
--- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
+++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
@@ -42,9 +42,9 @@ namespace osu.Game.Rulesets.Taiko.Audio
}
}
- private SkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd)
+ private PausableSkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd)
{
- var drawable = new SkinnableSound(hitSampleInfo)
+ var drawable = new PausableSkinnableSound(hitSampleInfo)
{
LifetimeStart = lifetimeStart,
LifetimeEnd = lifetimeEnd
@@ -57,8 +57,8 @@ namespace osu.Game.Rulesets.Taiko.Audio
public class DrumSample
{
- public SkinnableSound Centre;
- public SkinnableSound Rim;
+ public PausableSkinnableSound Centre;
+ public PausableSkinnableSound Rim;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs
index b595f43fbb..16a0726c8c 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
@@ -22,20 +21,20 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
new BeatmapStatistic
{
Name = @"Hit Count",
+ CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
Content = hits.ToString(),
- Icon = FontAwesome.Regular.Circle
},
new BeatmapStatistic
{
Name = @"Drumroll Count",
+ CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
Content = drumrolls.ToString(),
- Icon = FontAwesome.Regular.Circle
},
new BeatmapStatistic
{
Name = @"Swell Count",
+ CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
Content = swells.ToString(),
- Icon = FontAwesome.Regular.Circle
}
};
}
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index 2a1aa5d1df..ed7b8589ba 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -8,6 +8,8 @@ using osu.Game.Rulesets.Taiko.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Utils;
+using System.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
@@ -48,14 +50,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
public override bool CanConvert() => true;
- protected override Beatmap ConvertBeatmap(IBeatmap original)
+ protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
// Rewrite the beatmap info to add the slider velocity multiplier
original.BeatmapInfo = original.BeatmapInfo.Clone();
original.BeatmapInfo.BaseDifficulty = original.BeatmapInfo.BaseDifficulty.Clone();
original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= LEGACY_VELOCITY_MULTIPLIER;
- Beatmap converted = base.ConvertBeatmap(original);
+ Beatmap converted = base.ConvertBeatmap(original, cancellationToken);
if (original.BeatmapInfo.RulesetID == 3)
{
@@ -72,7 +74,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
return converted;
}
- protected override IEnumerable ConvertHitObject(HitObject obj, IBeatmap beatmap)
+ protected override IEnumerable ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken)
{
// Old osu! used hit sounding to determine various hit type information
IList samples = obj.Samples;
@@ -104,6 +106,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
};
i = (i + 1) % allSamples.Count;
+
+ if (Precision.AlmostEquals(0, tickSpacing))
+ break;
}
}
else
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs
new file mode 100644
index 0000000000..3b1a9ad777
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs
@@ -0,0 +1,145 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Taiko.Objects;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
+{
+ ///
+ /// Detects special hit object patterns which are easier to hit using special techniques
+ /// than normally assumed in the fully-alternating play style.
+ ///
+ ///
+ /// This component detects two basic types of patterns, leveraged by the following techniques:
+ ///
+ /// - Rolling allows hitting patterns with quickly and regularly alternating notes with a single hand.
+ /// - TL tapping makes hitting longer sequences of consecutive same-colour notes with little to no colour changes in-between.
+ ///
+ ///
+ public class StaminaCheeseDetector
+ {
+ ///
+ /// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a roll.
+ ///
+ private const int roll_min_repetitions = 12;
+
+ ///
+ /// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a TL tap.
+ ///
+ private const int tl_min_repetitions = 16;
+
+ ///
+ /// The list of all s in the map.
+ ///
+ private readonly List hitObjects;
+
+ public StaminaCheeseDetector(List hitObjects)
+ {
+ this.hitObjects = hitObjects;
+ }
+
+ ///
+ /// Finds and marks all objects in that special difficulty-reducing techiques apply to
+ /// with the flag.
+ ///
+ public void FindCheese()
+ {
+ findRolls(3);
+ findRolls(4);
+
+ findTlTap(0, HitType.Rim);
+ findTlTap(1, HitType.Rim);
+ findTlTap(0, HitType.Centre);
+ findTlTap(1, HitType.Centre);
+ }
+
+ ///
+ /// Finds and marks all sequences hittable using a roll.
+ ///
+ /// The length of a single repeating pattern to consider (triplets/quadruplets).
+ private void findRolls(int patternLength)
+ {
+ var history = new LimitedCapacityQueue(2 * patternLength);
+
+ // for convenience, we're tracking the index of the item *before* our suspected repeat's start,
+ // as that index can be simply subtracted from the current index to get the number of elements in between
+ // without off-by-one errors
+ int indexBeforeLastRepeat = -1;
+ int lastMarkEnd = 0;
+
+ for (int i = 0; i < hitObjects.Count; i++)
+ {
+ history.Enqueue(hitObjects[i]);
+ if (!history.Full)
+ continue;
+
+ if (!containsPatternRepeat(history, patternLength))
+ {
+ // we're setting this up for the next iteration, hence the +1.
+ // right here this index will point at the queue's front (oldest item),
+ // but that item is about to be popped next loop with an enqueue.
+ indexBeforeLastRepeat = i - history.Count + 1;
+ continue;
+ }
+
+ int repeatedLength = i - indexBeforeLastRepeat;
+ if (repeatedLength < roll_min_repetitions)
+ continue;
+
+ markObjectsAsCheese(Math.Max(lastMarkEnd, i - repeatedLength + 1), i);
+ lastMarkEnd = i;
+ }
+ }
+
+ ///
+ /// Determines whether the objects stored in contain a repetition of a pattern of length .
+ ///
+ private static bool containsPatternRepeat(LimitedCapacityQueue history, int patternLength)
+ {
+ for (int j = 0; j < patternLength; j++)
+ {
+ if (history[j].HitType != history[j + patternLength].HitType)
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Finds and marks all sequences hittable using a TL tap.
+ ///
+ /// Whether sequences starting with an odd- (1) or even-indexed (0) hit object should be checked.
+ /// The type of hit to check for TL taps.
+ private void findTlTap(int parity, HitType type)
+ {
+ int tlLength = -2;
+ int lastMarkEnd = 0;
+
+ for (int i = parity; i < hitObjects.Count; i += 2)
+ {
+ if (hitObjects[i].HitType == type)
+ tlLength += 2;
+ else
+ tlLength = -2;
+
+ if (tlLength < tl_min_repetitions)
+ continue;
+
+ markObjectsAsCheese(Math.Max(lastMarkEnd, i - tlLength + 1), i);
+ lastMarkEnd = i;
+ }
+ }
+
+ ///
+ /// Marks all objects from to (inclusive) as .
+ ///
+ private void markObjectsAsCheese(int start, int end)
+ {
+ for (int i = start; i <= end; i++)
+ hitObjects[i].StaminaCheese = true;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
index 6807142327..ae33c184d0 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
@@ -1,20 +1,94 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
{
+ ///
+ /// Represents a single hit object in taiko difficulty calculation.
+ ///
public class TaikoDifficultyHitObject : DifficultyHitObject
{
- public readonly bool HasTypeChange;
+ ///
+ /// The rhythm required to hit this hit object.
+ ///
+ public readonly TaikoDifficultyHitObjectRhythm Rhythm;
- public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate)
+ ///
+ /// The hit type of this hit object.
+ ///
+ public readonly HitType? HitType;
+
+ ///
+ /// The index of the object in the beatmap.
+ ///
+ public readonly int ObjectIndex;
+
+ ///
+ /// Whether the object should carry a penalty due to being hittable using special techniques
+ /// making it easier to do so.
+ ///
+ public bool StaminaCheese;
+
+ ///
+ /// Creates a new difficulty hit object.
+ ///
+ /// The gameplay associated with this difficulty object.
+ /// The gameplay preceding .
+ /// The gameplay preceding .
+ /// The rate of the gameplay clock. Modified by speed-changing mods.
+ /// The index of the object in the beatmap.
+ public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int objectIndex)
: base(hitObject, lastObject, clockRate)
{
- HasTypeChange = (lastObject as Hit)?.Type != (hitObject as Hit)?.Type;
+ var currentHit = hitObject as Hit;
+
+ Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate);
+ HitType = currentHit?.Type;
+
+ ObjectIndex = objectIndex;
+ }
+
+ ///
+ /// List of most common rhythm changes in taiko maps.
+ ///
+ ///
+ /// The general guidelines for the values are:
+ ///
+ /// - rhythm changes with ratio closer to 1 (that are not 1) are harder to play,
+ /// - speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch).
+ ///
+ ///
+ private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms =
+ {
+ new TaikoDifficultyHitObjectRhythm(1, 1, 0.0),
+ new TaikoDifficultyHitObjectRhythm(2, 1, 0.3),
+ new TaikoDifficultyHitObjectRhythm(1, 2, 0.5),
+ new TaikoDifficultyHitObjectRhythm(3, 1, 0.3),
+ new TaikoDifficultyHitObjectRhythm(1, 3, 0.35),
+ new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), // purposefully higher (requires hand switch in full alternating gameplay style)
+ new TaikoDifficultyHitObjectRhythm(2, 3, 0.4),
+ new TaikoDifficultyHitObjectRhythm(5, 4, 0.5),
+ new TaikoDifficultyHitObjectRhythm(4, 5, 0.7)
+ };
+
+ ///
+ /// Returns the closest rhythm change from required to hit this object.
+ ///
+ /// The gameplay preceding this one.
+ /// The gameplay preceding .
+ /// The rate of the gameplay clock.
+ private TaikoDifficultyHitObjectRhythm getClosestRhythm(HitObject lastObject, HitObject lastLastObject, double clockRate)
+ {
+ double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate;
+ double ratio = DeltaTime / prevLength;
+
+ return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First();
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs
new file mode 100644
index 0000000000..ea6a224094
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
+{
+ ///
+ /// Represents a rhythm change in a taiko map.
+ ///
+ public class TaikoDifficultyHitObjectRhythm
+ {
+ ///
+ /// The difficulty multiplier associated with this rhythm change.
+ ///
+ public readonly double Difficulty;
+
+ ///
+ /// The ratio of current
+ /// to previous for the rhythm change.
+ /// A above 1 indicates a slow-down; a below 1 indicates a speed-up.
+ ///
+ public readonly double Ratio;
+
+ ///
+ /// Creates an object representing a rhythm change.
+ ///
+ /// The numerator for .
+ /// The denominator for
+ /// The difficulty multiplier associated with this rhythm change.
+ public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty)
+ {
+ Ratio = numerator / (double)denominator;
+ Difficulty = difficulty;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
new file mode 100644
index 0000000000..32421ee00a
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
@@ -0,0 +1,135 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Taiko.Objects;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
+{
+ ///
+ /// Calculates the colour coefficient of taiko difficulty.
+ ///
+ public class Colour : Skill
+ {
+ protected override double SkillMultiplier => 1;
+ protected override double StrainDecayBase => 0.4;
+
+ ///
+ /// Maximum number of entries to keep in .
+ ///
+ private const int mono_history_max_length = 5;
+
+ ///
+ /// Queue with the lengths of the last most recent mono (single-colour) patterns,
+ /// with the most recent value at the end of the queue.
+ ///
+ private readonly LimitedCapacityQueue monoHistory = new LimitedCapacityQueue(mono_history_max_length);
+
+ ///
+ /// The of the last object hit before the one being considered.
+ ///
+ private HitType? previousHitType;
+
+ ///
+ /// Length of the current mono pattern.
+ ///
+ private int currentMonoLength;
+
+ protected override double StrainValueOf(DifficultyHitObject current)
+ {
+ // changing from/to a drum roll or a swell does not constitute a colour change.
+ // hits spaced more than a second apart are also exempt from colour strain.
+ if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000))
+ {
+ monoHistory.Clear();
+
+ var currentHit = current.BaseObject as Hit;
+ currentMonoLength = currentHit != null ? 1 : 0;
+ previousHitType = currentHit?.Type;
+
+ return 0.0;
+ }
+
+ var taikoCurrent = (TaikoDifficultyHitObject)current;
+
+ double objectStrain = 0.0;
+
+ if (previousHitType != null && taikoCurrent.HitType != previousHitType)
+ {
+ // The colour has changed.
+ objectStrain = 1.0;
+
+ if (monoHistory.Count < 2)
+ {
+ // There needs to be at least two streaks to determine a strain.
+ objectStrain = 0.0;
+ }
+ else if ((monoHistory[^1] + currentMonoLength) % 2 == 0)
+ {
+ // The last streak in the history is guaranteed to be a different type to the current streak.
+ // If the total number of notes in the two streaks is even, nullify this object's strain.
+ objectStrain = 0.0;
+ }
+
+ objectStrain *= repetitionPenalties();
+ currentMonoLength = 1;
+ }
+ else
+ {
+ currentMonoLength += 1;
+ }
+
+ previousHitType = taikoCurrent.HitType;
+ return objectStrain;
+ }
+
+ ///
+ /// The penalty to apply due to the length of repetition in colour streaks.
+ ///
+ private double repetitionPenalties()
+ {
+ const int most_recent_patterns_to_compare = 2;
+ double penalty = 1.0;
+
+ monoHistory.Enqueue(currentMonoLength);
+
+ for (int start = monoHistory.Count - most_recent_patterns_to_compare - 1; start >= 0; start--)
+ {
+ if (!isSamePattern(start, most_recent_patterns_to_compare))
+ continue;
+
+ int notesSince = 0;
+ for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i];
+ penalty *= repetitionPenalty(notesSince);
+ break;
+ }
+
+ return penalty;
+ }
+
+ ///
+ /// Determines whether the last patterns have repeated in the history
+ /// of single-colour note sequences, starting from .
+ ///
+ private bool isSamePattern(int start, int mostRecentPatternsToCompare)
+ {
+ for (int i = 0; i < mostRecentPatternsToCompare; i++)
+ {
+ if (monoHistory[start + i] != monoHistory[monoHistory.Count - mostRecentPatternsToCompare + i])
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Calculates the strain penalty for a colour pattern repetition.
+ ///
+ /// The number of notes since the last repetition of the pattern.
+ private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince);
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
new file mode 100644
index 0000000000..5569b27ad5
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
@@ -0,0 +1,167 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Taiko.Objects;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
+{
+ ///
+ /// Calculates the rhythm coefficient of taiko difficulty.
+ ///
+ public class Rhythm : Skill
+ {
+ protected override double SkillMultiplier => 10;
+ protected override double StrainDecayBase => 0;
+
+ ///
+ /// The note-based decay for rhythm strain.
+ ///
+ ///
+ /// is not used here, as it's time- and not note-based.
+ ///
+ private const double strain_decay = 0.96;
+
+ ///
+ /// Maximum number of entries in .
+ ///
+ private const int rhythm_history_max_length = 8;
+
+ ///
+ /// Contains the last changes in note sequence rhythms.
+ ///
+ private readonly LimitedCapacityQueue rhythmHistory = new LimitedCapacityQueue(rhythm_history_max_length);
+
+ ///
+ /// Contains the rolling rhythm strain.
+ /// Used to apply per-note decay.
+ ///
+ private double currentStrain;
+
+ ///
+ /// Number of notes since the last rhythm change has taken place.
+ ///
+ private int notesSinceRhythmChange;
+
+ protected override double StrainValueOf(DifficultyHitObject current)
+ {
+ // drum rolls and swells are exempt.
+ if (!(current.BaseObject is Hit))
+ {
+ resetRhythmAndStrain();
+ return 0.0;
+ }
+
+ currentStrain *= strain_decay;
+
+ TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
+ notesSinceRhythmChange += 1;
+
+ // rhythm difficulty zero (due to rhythm not changing) => no rhythm strain.
+ if (hitObject.Rhythm.Difficulty == 0.0)
+ {
+ return 0.0;
+ }
+
+ double objectStrain = hitObject.Rhythm.Difficulty;
+
+ objectStrain *= repetitionPenalties(hitObject);
+ objectStrain *= patternLengthPenalty(notesSinceRhythmChange);
+ objectStrain *= speedPenalty(hitObject.DeltaTime);
+
+ // careful - needs to be done here since calls above read this value
+ notesSinceRhythmChange = 0;
+
+ currentStrain += objectStrain;
+ return currentStrain;
+ }
+
+ ///
+ /// Returns a penalty to apply to the current hit object caused by repeating rhythm changes.
+ ///
+ ///
+ /// Repetitions of more recent patterns are associated with a higher penalty.
+ ///
+ /// The current hit object being considered.
+ private double repetitionPenalties(TaikoDifficultyHitObject hitObject)
+ {
+ double penalty = 1;
+
+ rhythmHistory.Enqueue(hitObject);
+
+ for (int mostRecentPatternsToCompare = 2; mostRecentPatternsToCompare <= rhythm_history_max_length / 2; mostRecentPatternsToCompare++)
+ {
+ for (int start = rhythmHistory.Count - mostRecentPatternsToCompare - 1; start >= 0; start--)
+ {
+ if (!samePattern(start, mostRecentPatternsToCompare))
+ continue;
+
+ int notesSince = hitObject.ObjectIndex - rhythmHistory[start].ObjectIndex;
+ penalty *= repetitionPenalty(notesSince);
+ break;
+ }
+ }
+
+ return penalty;
+ }
+
+ ///
+ /// Determines whether the rhythm change pattern starting at is a repeat of any of the
+ /// .
+ ///
+ private bool samePattern(int start, int mostRecentPatternsToCompare)
+ {
+ for (int i = 0; i < mostRecentPatternsToCompare; i++)
+ {
+ if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - mostRecentPatternsToCompare + i].Rhythm)
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Calculates a single rhythm repetition penalty.
+ ///
+ /// Number of notes since the last repetition of a rhythm change.
+ private static double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince);
+
+ ///
+ /// Calculates a penalty based on the number of notes since the last rhythm change.
+ /// Both rare and frequent rhythm changes are penalised.
+ ///
+ /// Number of notes since the last rhythm change.
+ private static double patternLengthPenalty(int patternLength)
+ {
+ double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0);
+ double longPatternPenalty = Math.Clamp(2.5 - 0.15 * patternLength, 0.0, 1.0);
+ return Math.Min(shortPatternPenalty, longPatternPenalty);
+ }
+
+ ///
+ /// Calculates a penalty for objects that do not require alternating hands.
+ ///
+ /// Time (in milliseconds) since the last hit object.
+ private double speedPenalty(double deltaTime)
+ {
+ if (deltaTime < 80) return 1;
+ if (deltaTime < 210) return Math.Max(0, 1.4 - 0.005 * deltaTime);
+
+ resetRhythmAndStrain();
+ return 0.0;
+ }
+
+ ///
+ /// Resets the rolling strain value and counter.
+ ///
+ private void resetRhythmAndStrain()
+ {
+ currentStrain = 0.0;
+ notesSinceRhythmChange = 0;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
new file mode 100644
index 0000000000..0b61eb9930
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
@@ -0,0 +1,113 @@
+// 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.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Taiko.Objects;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
+{
+ ///
+ /// Calculates the stamina coefficient of taiko difficulty.
+ ///
+ ///
+ /// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit).
+ ///
+ public class Stamina : Skill
+ {
+ protected override double SkillMultiplier => 1;
+ protected override double StrainDecayBase => 0.4;
+
+ ///
+ /// Maximum number of entries to keep in .
+ ///
+ private const int max_history_length = 2;
+
+ ///
+ /// The index of the hand this instance is associated with.
+ ///
+ ///
+ /// The value of 0 indicates the left hand (full alternating gameplay starting with left hand is assumed).
+ /// This naturally translates onto index offsets of the objects in the map.
+ ///
+ private readonly int hand;
+
+ ///
+ /// Stores the last durations between notes hit with the hand indicated by .
+ ///
+ private readonly LimitedCapacityQueue notePairDurationHistory = new LimitedCapacityQueue(max_history_length);
+
+ ///
+ /// Stores the of the last object that was hit by the other hand.
+ ///
+ private double offhandObjectDuration = double.MaxValue;
+
+ ///
+ /// Creates a skill.
+ ///
+ /// Whether this instance is performing calculations for the right hand.
+ public Stamina(bool rightHand)
+ {
+ hand = rightHand ? 1 : 0;
+ }
+
+ protected override double StrainValueOf(DifficultyHitObject current)
+ {
+ if (!(current.BaseObject is Hit))
+ {
+ return 0.0;
+ }
+
+ TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
+
+ if (hitObject.ObjectIndex % 2 == hand)
+ {
+ double objectStrain = 1;
+
+ if (hitObject.ObjectIndex == 1)
+ return 1;
+
+ notePairDurationHistory.Enqueue(hitObject.DeltaTime + offhandObjectDuration);
+
+ double shortestRecentNote = notePairDurationHistory.Min();
+ objectStrain += speedBonus(shortestRecentNote);
+
+ if (hitObject.StaminaCheese)
+ objectStrain *= cheesePenalty(hitObject.DeltaTime + offhandObjectDuration);
+
+ return objectStrain;
+ }
+
+ offhandObjectDuration = hitObject.DeltaTime;
+ return 0;
+ }
+
+ ///
+ /// Applies a penalty for hit objects marked with .
+ ///
+ /// The duration between the current and previous note hit using the hand indicated by .
+ private double cheesePenalty(double notePairDuration)
+ {
+ if (notePairDuration > 125) return 1;
+ if (notePairDuration < 100) return 0.6;
+
+ return 0.6 + (notePairDuration - 100) * 0.016;
+ }
+
+ ///
+ /// Applies a speed bonus dependent on the time since the last hit performed using this hand.
+ ///
+ /// The duration between the current and previous note hit using the hand indicated by .
+ private double speedBonus(double notePairDuration)
+ {
+ if (notePairDuration >= 200) return 0;
+
+ double bonus = 200 - notePairDuration;
+ bonus *= bonus;
+ return bonus / 100000;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs
deleted file mode 100644
index 2c1885ae1a..0000000000
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using osu.Game.Rulesets.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Difficulty.Skills;
-using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Taiko.Objects;
-
-namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
-{
- public class Strain : Skill
- {
- private const double rhythm_change_base_threshold = 0.2;
- private const double rhythm_change_base = 2.0;
-
- protected override double SkillMultiplier => 1;
- protected override double StrainDecayBase => 0.3;
-
- private ColourSwitch lastColourSwitch = ColourSwitch.None;
-
- private int sameColourCount = 1;
-
- protected override double StrainValueOf(DifficultyHitObject current)
- {
- double addition = 1;
-
- // We get an extra addition if we are not a slider or spinner
- if (current.LastObject is Hit && current.BaseObject is Hit && current.BaseObject.StartTime - current.LastObject.StartTime < 1000)
- {
- if (hasColourChange(current))
- addition += 0.75;
-
- if (hasRhythmChange(current))
- addition += 1;
- }
- else
- {
- lastColourSwitch = ColourSwitch.None;
- sameColourCount = 1;
- }
-
- double additionFactor = 1;
-
- // Scale the addition factor linearly from 0.4 to 1 for DeltaTime from 0 to 50
- if (current.DeltaTime < 50)
- additionFactor = 0.4 + 0.6 * current.DeltaTime / 50;
-
- return additionFactor * addition;
- }
-
- private bool hasRhythmChange(DifficultyHitObject current)
- {
- // We don't want a division by zero if some random mapper decides to put two HitObjects at the same time.
- if (current.DeltaTime == 0 || Previous.Count == 0 || Previous[0].DeltaTime == 0)
- return false;
-
- double timeElapsedRatio = Math.Max(Previous[0].DeltaTime / current.DeltaTime, current.DeltaTime / Previous[0].DeltaTime);
-
- if (timeElapsedRatio >= 8)
- return false;
-
- double difference = Math.Log(timeElapsedRatio, rhythm_change_base) % 1.0;
-
- return difference > rhythm_change_base_threshold && difference < 1 - rhythm_change_base_threshold;
- }
-
- private bool hasColourChange(DifficultyHitObject current)
- {
- var taikoCurrent = (TaikoDifficultyHitObject)current;
-
- if (!taikoCurrent.HasTypeChange)
- {
- sameColourCount++;
- return false;
- }
-
- var oldColourSwitch = lastColourSwitch;
- var newColourSwitch = sameColourCount % 2 == 0 ? ColourSwitch.Even : ColourSwitch.Odd;
-
- lastColourSwitch = newColourSwitch;
- sameColourCount = 1;
-
- // We only want a bonus if the parity of the color switch changes
- return oldColourSwitch != ColourSwitch.None && oldColourSwitch != newColourSwitch;
- }
-
- private enum ColourSwitch
- {
- None,
- Even,
- Odd
- }
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
index 75d3807bba..5bed48bcc6 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
@@ -7,7 +7,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
public class TaikoDifficultyAttributes : DifficultyAttributes
{
+ public double StaminaStrain;
+ public double RhythmStrain;
+ public double ColourStrain;
+ public double ApproachRate;
public double GreatHitWindow;
- public int MaxCombo;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index 32d49ea39c..e5485db4df 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@@ -19,39 +20,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
public class TaikoDifficultyCalculator : DifficultyCalculator
{
- private const double star_scaling_factor = 0.04125;
+ private const double rhythm_skill_multiplier = 0.014;
+ private const double colour_skill_multiplier = 0.01;
+ private const double stamina_skill_multiplier = 0.02;
public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}
- protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
+ protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
{
- if (beatmap.HitObjects.Count == 0)
- return new TaikoDifficultyAttributes { Mods = mods, Skills = skills };
-
- HitWindows hitWindows = new TaikoHitWindows();
- hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
-
- return new TaikoDifficultyAttributes
- {
- StarRating = skills.Single().DifficultyValue() * star_scaling_factor,
- Mods = mods,
- // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
- GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate,
- MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
- Skills = skills
- };
- }
-
- protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
- {
- for (int i = 1; i < beatmap.HitObjects.Count; i++)
- yield return new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate);
- }
-
- protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] { new Strain() };
+ new Colour(),
+ new Rhythm(),
+ new Stamina(true),
+ new Stamina(false),
+ };
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
{
@@ -60,5 +44,124 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
new TaikoModEasy(),
new TaikoModHardRock(),
};
+
+ protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
+ {
+ List taikoDifficultyHitObjects = new List();
+
+ for (int i = 2; i < beatmap.HitObjects.Count; i++)
+ {
+ taikoDifficultyHitObjects.Add(
+ new TaikoDifficultyHitObject(
+ beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, i
+ )
+ );
+ }
+
+ new StaminaCheeseDetector(taikoDifficultyHitObjects).FindCheese();
+ return taikoDifficultyHitObjects;
+ }
+
+ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
+ {
+ if (beatmap.HitObjects.Count == 0)
+ return new TaikoDifficultyAttributes { Mods = mods, Skills = skills };
+
+ var colour = (Colour)skills[0];
+ var rhythm = (Rhythm)skills[1];
+ var staminaRight = (Stamina)skills[2];
+ var staminaLeft = (Stamina)skills[3];
+
+ double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
+ double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
+ double staminaRating = (staminaRight.DifficultyValue() + staminaLeft.DifficultyValue()) * stamina_skill_multiplier;
+
+ double staminaPenalty = simpleColourPenalty(staminaRating, colourRating);
+ staminaRating *= staminaPenalty;
+
+ double combinedRating = locallyCombinedDifficulty(colour, rhythm, staminaRight, staminaLeft, staminaPenalty);
+ double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating);
+ double starRating = 1.4 * separatedRating + 0.5 * combinedRating;
+ starRating = rescale(starRating);
+
+ HitWindows hitWindows = new TaikoHitWindows();
+ hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
+
+ return new TaikoDifficultyAttributes
+ {
+ StarRating = starRating,
+ Mods = mods,
+ StaminaStrain = staminaRating,
+ RhythmStrain = rhythmRating,
+ ColourStrain = colourRating,
+ // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
+ GreatHitWindow = (int)hitWindows.WindowFor(HitResult.Great) / clockRate,
+ MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
+ Skills = skills
+ };
+ }
+
+ ///
+ /// Calculates the penalty for the stamina skill for maps with low colour difficulty.
+ ///
+ ///
+ /// Some maps (especially converts) can be easy to read despite a high note density.
+ /// This penalty aims to reduce the star rating of such maps by factoring in colour difficulty to the stamina skill.
+ ///
+ private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty)
+ {
+ if (colorDifficulty <= 0) return 0.79 - 0.25;
+
+ return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2;
+ }
+
+ ///
+ /// Returns the p-norm of an n-dimensional vector.
+ ///
+ /// The value of p to calculate the norm for.
+ /// The coefficients of the vector.
+ private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
+
+ ///
+ /// Returns the partial star rating of the beatmap, calculated using peak strains from all sections of the map.
+ ///
+ ///
+ /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
+ /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
+ ///
+ private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina staminaRight, Stamina staminaLeft, double staminaPenalty)
+ {
+ List peaks = new List();
+
+ for (int i = 0; i < colour.StrainPeaks.Count; i++)
+ {
+ double colourPeak = colour.StrainPeaks[i] * colour_skill_multiplier;
+ double rhythmPeak = rhythm.StrainPeaks[i] * rhythm_skill_multiplier;
+ double staminaPeak = (staminaRight.StrainPeaks[i] + staminaLeft.StrainPeaks[i]) * stamina_skill_multiplier * staminaPenalty;
+ peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak));
+ }
+
+ double difficulty = 0;
+ double weight = 1;
+
+ foreach (double strain in peaks.OrderByDescending(d => d))
+ {
+ difficulty += strain * weight;
+ weight *= 0.9;
+ }
+
+ return difficulty;
+ }
+
+ ///
+ /// Applies a final re-scaling of the star rating to bring maps with recorded full combos below 9.5 stars.
+ ///
+ /// The raw star rating value before re-scaling.
+ private double rescale(double sr)
+ {
+ if (sr < 0) return sr;
+
+ return 10.43 * Math.Log(sr / 8 + 1);
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index bc147b53ac..2d9b95ae88 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
-using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@@ -20,12 +19,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private Mod[] mods;
private int countGreat;
- private int countGood;
+ private int countOk;
private int countMeh;
private int countMiss;
- public TaikoPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score)
- : base(ruleset, beatmap, score)
+ public TaikoPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
+ : base(ruleset, attributes, score)
{
}
@@ -33,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
mods = Score.Mods;
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);
@@ -78,10 +77,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
strainValue *= Math.Pow(0.985, countMiss);
- // Combo scaling
- if (Attributes.MaxCombo > 0)
- strainValue *= Math.Min(Math.Pow(Score.MaxCombo, 0.5) / Math.Pow(Attributes.MaxCombo, 0.5), 1.0);
-
if (mods.Any(m => m is ModHidden))
strainValue *= 1.025;
@@ -106,6 +101,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
}
- private int totalHits => countGreat + countGood + countMeh + countMiss;
+ private int totalHits => countGreat + countOk + countMeh + countMiss;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs
index bf77c76670..587a4efecb 100644
--- a/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
@@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
}
+ public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
+
public override PlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint();
}
}
diff --git a/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs
index e877cf6240..3e97b4e322 100644
--- a/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
@@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
}
+ public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
+
public override PlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint();
}
}
diff --git a/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs
index a6191fcedc..918afde1dd 100644
--- a/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
@@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
}
+ public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
+
public override PlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint();
}
}
diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
index eebf6980fe..d5dd758e10 100644
--- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
@@ -1,9 +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 System.Collections.Generic;
using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
@@ -14,67 +15,86 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
public class TaikoSelectionHandler : SelectionHandler
{
+ private readonly Bindable selectionRimState = new Bindable();
+ private readonly Bindable selectionStrongState = new Bindable();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ selectionStrongState.ValueChanged += state =>
+ {
+ switch (state.NewValue)
+ {
+ case TernaryState.False:
+ SetStrongState(false);
+ break;
+
+ case TernaryState.True:
+ SetStrongState(true);
+ break;
+ }
+ };
+
+ selectionRimState.ValueChanged += state =>
+ {
+ switch (state.NewValue)
+ {
+ case TernaryState.False:
+ SetRimState(false);
+ break;
+
+ case TernaryState.True:
+ SetRimState(true);
+ break;
+ }
+ };
+ }
+
+ public void SetStrongState(bool state)
+ {
+ var hits = SelectedHitObjects.OfType();
+
+ ChangeHandler.BeginChange();
+
+ foreach (var h in hits)
+ {
+ if (h.IsStrong != state)
+ {
+ h.IsStrong = state;
+ EditorBeatmap.UpdateHitObject(h);
+ }
+ }
+
+ ChangeHandler.EndChange();
+ }
+
+ public void SetRimState(bool state)
+ {
+ var hits = SelectedHitObjects.OfType();
+
+ ChangeHandler.BeginChange();
+
+ foreach (var h in hits)
+ h.Type = state ? HitType.Rim : HitType.Centre;
+
+ ChangeHandler.EndChange();
+ }
+
protected override IEnumerable