1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-07 23:23:12 +08:00

Merge branch 'master' into tourney-asset-refactor

This commit is contained in:
Dean Herbert 2020-10-07 21:34:07 +09:00 committed by GitHub
commit 1989141968
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
774 changed files with 17705 additions and 4787 deletions

View File

@ -192,3 +192,6 @@ dotnet_diagnostic.IDE0052.severity = silent
dotnet_diagnostic.IDE0067.severity = none dotnet_diagnostic.IDE0067.severity = none
dotnet_diagnostic.IDE0068.severity = none dotnet_diagnostic.IDE0068.severity = none
dotnet_diagnostic.IDE0069.severity = none dotnet_diagnostic.IDE0069.severity = none
#Disable operator overloads requiring alternate named methods
dotnet_diagnostic.CA2225.severity = none

View File

@ -16,7 +16,7 @@
<EmbeddedResource Include="Resources\**\*.*" /> <EmbeddedResource Include="Resources\**\*.*" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Code Analysis"> <ItemGroup Label="Code Analysis">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.0" PrivateAssets="All" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" /> <AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.0.0" PrivateAssets="All" />
</ItemGroup> </ItemGroup>

View File

@ -6,35 +6,36 @@ GEM
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.1.0) aws-eventstream (1.1.0)
aws-partitions (1.329.0) aws-partitions (1.354.0)
aws-sdk-core (3.99.2) aws-sdk-core (3.104.3)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0) aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-kms (1.34.1) aws-sdk-kms (1.36.0)
aws-sdk-core (~> 3, >= 3.99.0) aws-sdk-core (~> 3, >= 3.99.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.68.1) aws-sdk-s3 (1.78.0)
aws-sdk-core (~> 3, >= 3.99.0) aws-sdk-core (~> 3, >= 3.104.3)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.4) aws-sigv4 (1.2.1)
aws-eventstream (~> 1.0, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.3) babosa (1.0.3)
claide (1.0.3) claide (1.0.3)
colored (1.2) colored (1.2)
colored2 (3.1.2) colored2 (3.1.2)
commander-fastlane (4.4.6) commander-fastlane (4.4.6)
highline (~> 1.7.2) highline (~> 1.7.2)
declarative (0.0.10) declarative (0.0.20)
declarative-option (0.1.0) declarative-option (0.1.0)
digest-crc (0.5.1) digest-crc (0.6.1)
rake (~> 13.0)
domain_name (0.5.20190701) domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.5) dotenv (2.7.6)
emoji_regex (1.0.1) emoji_regex (3.0.0)
excon (0.74.0) excon (0.76.0)
faraday (1.0.1) faraday (1.0.1)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6) faraday-cookie_jar (0.0.6)
@ -42,34 +43,32 @@ GEM
http-cookie (~> 1.0.0) http-cookie (~> 1.0.0)
faraday_middleware (1.0.0) faraday_middleware (1.0.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.1.7) fastimage (2.2.0)
fastlane (2.149.1) fastlane (2.156.0)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0) addressable (>= 2.3, < 3.0.0)
aws-sdk-s3 (~> 1.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) bundler (>= 1.12.0, < 3.0.0)
colored colored
commander-fastlane (>= 4.4.6, < 5.0.0) commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.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) excon (>= 0.71.0, < 1.0.0)
faraday (>= 0.17, < 2.0) faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6) faraday-cookie_jar (~> 0.0.6)
faraday_middleware (>= 0.13.1, < 2.0) faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0) fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0) gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.37.0, < 0.39.0) google-api-client (>= 0.37.0, < 0.39.0)
google-cloud-storage (>= 1.15.0, < 2.0.0) google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0) highline (>= 1.7.2, < 2.0.0)
json (< 3.0.0) json (< 3.0.0)
jwt (~> 2.1.0) jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0) mini_magick (>= 4.9.4, < 5.0.0)
multi_xml (~> 0.5)
multipart-post (~> 2.0.0) multipart-post (~> 2.0.0)
plist (>= 3.1.0, < 4.0.0) plist (>= 3.1.0, < 4.0.0)
public_suffix (~> 2.0.0) rubyzip (>= 2.0.0, < 3.0.0)
rubyzip (>= 1.3.0, < 2.0.0)
security (= 0.1.3) security (= 0.1.3)
simctl (~> 1.6.3) simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0) slack-notifier (>= 2.0.0, < 3.0.0)
@ -97,17 +96,17 @@ GEM
google-cloud-core (1.5.0) google-cloud-core (1.5.0)
google-cloud-env (~> 1.0) google-cloud-env (~> 1.0)
google-cloud-errors (~> 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) faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.1) google-cloud-errors (1.0.1)
google-cloud-storage (1.26.2) google-cloud-storage (1.27.0)
addressable (~> 2.5) addressable (~> 2.5)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-api-client (~> 0.33) google-api-client (~> 0.33)
google-cloud-core (~> 1.2) google-cloud-core (~> 1.2)
googleauth (~> 0.9) googleauth (~> 0.9)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (0.12.0) googleauth (0.13.1)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16) memoist (~> 0.16)
@ -119,29 +118,29 @@ GEM
domain_name (~> 0.5) domain_name (~> 0.5)
httpclient (2.8.3) httpclient (2.8.3)
jmespath (1.4.0) jmespath (1.4.0)
json (2.3.0) json (2.3.1)
jwt (2.1.0) jwt (2.2.1)
memoist (0.16.2) memoist (0.16.2)
mini_magick (4.10.1) mini_magick (4.10.1)
mini_mime (1.0.2) mini_mime (1.0.2)
mini_portile2 (2.4.0) mini_portile2 (2.4.0)
multi_json (1.14.1) multi_json (1.15.0)
multi_xml (0.6.0)
multipart-post (2.0.0) multipart-post (2.0.0)
nanaimo (0.2.6) nanaimo (0.3.0)
naturally (2.2.0) naturally (2.2.0)
nokogiri (1.10.7) nokogiri (1.10.10)
mini_portile2 (~> 2.4.0) mini_portile2 (~> 2.4.0)
os (1.1.0) os (1.1.1)
plist (3.5.0) plist (3.5.0)
public_suffix (2.0.5) public_suffix (4.0.5)
rake (13.0.1)
representable (3.0.4) representable (3.0.4)
declarative (< 0.1.0) declarative (< 0.1.0)
declarative-option (< 0.2.0) declarative-option (< 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
retriable (3.1.2) retriable (3.1.2)
rouge (2.0.7) rouge (2.0.7)
rubyzip (1.3.0) rubyzip (2.3.0)
security (0.1.3) security (0.1.3)
signet (0.14.0) signet (0.14.0)
addressable (~> 2.3) addressable (~> 2.3)
@ -160,7 +159,7 @@ GEM
terminal-table (1.8.0) terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (~> 1.1, >= 1.1.1)
tty-cursor (0.7.1) tty-cursor (0.7.1)
tty-screen (0.8.0) tty-screen (0.8.1)
tty-spinner (0.9.3) tty-spinner (0.9.3)
tty-cursor (~> 0.7) tty-cursor (~> 0.7)
uber (0.1.0) uber (0.1.0)
@ -169,12 +168,12 @@ GEM
unf_ext (0.0.7.7) unf_ext (0.0.7.7)
unicode-display_width (1.7.0) unicode-display_width (1.7.0)
word_wrap (1.0.0) word_wrap (1.0.0)
xcodeproj (1.16.0) xcodeproj (1.18.0)
CFPropertyList (>= 2.3.3, < 4.0) CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3) atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1) colored2 (~> 3.1)
nanaimo (~> 0.2.6) nanaimo (~> 0.3.0)
xcpretty (0.3.0) xcpretty (0.3.0)
rouge (~> 2.0.7) rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0) xcpretty-travis-formatter (1.0.0)

View File

@ -9,7 +9,9 @@
[![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu) [![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) [![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 ## 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. 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: Please make sure you have the following prerequisites:

View File

@ -113,7 +113,7 @@ platform :ios do
souyuz( souyuz(
platform: "ios", platform: "ios",
plist_path: "../osu.iOS/Info.plist" plist_path: "osu.iOS/Info.plist"
) )
end end
@ -127,7 +127,7 @@ platform :ios do
end end
lane :update_version do |options| lane :update_version do |options|
options[:plist_path] = '../osu.iOS/Info.plist' options[:plist_path] = 'osu.iOS/Info.plist'
app_version(options) app_version(options)
end end

View File

@ -5,6 +5,6 @@
"version": "3.1.100" "version": "3.1.100"
}, },
"msbuild-sdks": { "msbuild-sdks": {
"Microsoft.Build.Traversal": "2.0.52" "Microsoft.Build.Traversal": "2.1.1"
} }
} }

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.731.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.806.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.1004.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -9,7 +9,7 @@ using osu.Framework.Android;
namespace osu.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 public class OsuGameActivity : AndroidGameActivity
{ {
protected override Framework.Game CreateGame() => new OsuGameAndroid(); protected override Framework.Game CreateGame() => new OsuGameAndroid();

View File

@ -3,7 +3,7 @@
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Description>click the circles. to the beat.</Description> <Description>A free-to-win rhythm game. Rhythm is just a *click* away!</Description>
<AssemblyName>osu!</AssemblyName> <AssemblyName>osu!</AssemblyName>
<Title>osu!lazer</Title> <Title>osu!lazer</Title>
<Product>osu!lazer</Product> <Product>osu!lazer</Product>

View File

@ -9,8 +9,7 @@
<projectUrl>https://osu.ppy.sh/</projectUrl> <projectUrl>https://osu.ppy.sh/</projectUrl>
<iconUrl>https://puu.sh/tYyXZ/9a01a5d1b0.ico</iconUrl> <iconUrl>https://puu.sh/tYyXZ/9a01a5d1b0.ico</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>click the circles. to the beat.</description> <description>A free-to-win rhythm game. Rhythm is just a *click* away!</description>
<summary>click the circles.</summary>
<releaseNotes>testing</releaseNotes> <releaseNotes>testing</releaseNotes>
<copyright>Copyright (c) 2020 ppy Pty Ltd</copyright> <copyright>Copyright (c) 2020 ppy Pty Ltd</copyright>
<language>en-AU</language> <language>en-AU</language>

View File

@ -14,6 +14,7 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Catch.Tests namespace osu.Game.Rulesets.Catch.Tests
{ {
[TestFixture] [TestFixture]
[Timeout(10000)]
public class CatchBeatmapConversionTest : BeatmapConversionTest<ConvertValue> public class CatchBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; 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-stream", new[] { typeof(CatchModHardRock) })]
[TestCase("hardrock-repeat-slider", new[] { typeof(CatchModHardRock) })] [TestCase("hardrock-repeat-slider", new[] { typeof(CatchModHardRock) })]
[TestCase("hardrock-spinner", 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); public new void Test(string name, params Type[] mods) => base.Test(name, mods);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject) protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)

View File

@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Droplet { StartTime = 1000 }), shouldMiss); public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Droplet { StartTime = 1000 }), shouldMiss);
// We only care about testing misses, hits are tested via JuiceStream // 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); public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss);
} }
} }

View File

@ -0,0 +1,84 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<HitObject>
{
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<CatchPlayfield>().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;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
@ -38,7 +40,11 @@ namespace osu.Game.Rulesets.Catch.Tests
new Vector2(width, 0) new Vector2(width, 0)
}), }),
StartTime = i * 2000, StartTime = i * 2000,
NewCombo = i % 8 == 0 NewCombo = i % 8 == 0,
Samples = new List<HitSampleInfo>(new[]
{
new HitSampleInfo { Bank = "normal", Name = "hitnormal", Volume = 100 }
})
}); });
} }

View File

@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Catch.Tests
Schedule(() => Schedule(() =>
{ {
area.AttemptCatch(fruit); 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(); drawable.Expire();
}); });

View File

@ -0,0 +1,65 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<CatchComboDisplay>())
counter.OnNewResult(judgedObject, result);
}
}
}

View File

@ -20,19 +20,19 @@ namespace osu.Game.Rulesets.Catch.Tests
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
AddStep($"show {rep}", () => SetContents(() => createDrawable(rep))); AddStep($"show {rep}", () => SetContents(() => createDrawable(rep)));
AddStep("show droplet", () => SetContents(createDrawableDroplet)); AddStep("show droplet", () => SetContents(() => createDrawableDroplet()));
AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet)); AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet));
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawable(rep, true))); AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawable(rep, true)));
AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true)));
} }
private Drawable createDrawableTinyDroplet() private Drawable createDrawableTinyDroplet()
{ {
var droplet = new TinyDroplet var droplet = new TestCatchTinyDroplet
{ {
StartTime = Clock.CurrentTime,
Scale = 1.5f, 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, Scale = 1.5f,
HyperDashTarget = hyperdash ? new Banana() : null
}; };
return new DrawableDroplet(droplet) return new DrawableDroplet(droplet)
@ -95,5 +95,21 @@ namespace osu.Game.Rulesets.Catch.Tests
public override FruitVisualRepresentation VisualRepresentation { get; } public override FruitVisualRepresentation VisualRepresentation { get; }
} }
public class TestCatchDroplet : Droplet
{
public TestCatchDroplet()
{
StartTime = 1000000000000;
}
}
public class TestCatchTinyDroplet : TinyDroplet
{
public TestCatchTinyDroplet()
{
StartTime = 1000000000000;
}
}
} }
} }

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -18,22 +19,42 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
protected override bool Autoplay => true; protected override bool Autoplay => true;
private int hyperDashCount;
private bool inHyperDash;
[Test] [Test]
public void TestHyperDash() public void TestHyperDash()
{ {
AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash); AddStep("reset count", () =>
AddUntilStep("wait for right movement", () => getCatcher().Scale.X > 0); // don't check hyperdashing as it happens too fast.
AddUntilStep("wait for left movement", () => getCatcher().Scale.X < 0);
for (int i = 0; i < 3; i++)
{ {
AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing); inHyperDash = false;
AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing); hyperDashCount = 0;
}
}
private Catcher getCatcher() => Player.ChildrenOfType<CatcherArea>().First().MovableCatcher; // 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<CatcherArea>().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);
}
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{ {
@ -46,6 +67,8 @@ namespace osu.Game.Rulesets.Catch.Tests
} }
}; };
beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
// Should produce a hyper-dash (edge case test) // 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 = 1816, X = 56, NewCombo = true });
beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308, 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 Fruit { X = right_x });
createObjects(() => new TestJuiceStream(left_x), 1); 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; return beatmap;
void createObjects(Func<CatchHitObject> createObject, int count = 3) void createObjects(Func<CatchHitObject> createObject, int count = 3)

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
@ -23,19 +22,19 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{ {
Name = @"Fruit Count", Name = @"Fruit Count",
Content = fruits.ToString(), Content = fruits.ToString(),
Icon = FontAwesome.Regular.Circle CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
}, },
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Juice Stream Count", Name = @"Juice Stream Count",
Content = juiceStreams.ToString(), Content = juiceStreams.ToString(),
Icon = FontAwesome.Regular.Circle CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
}, },
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Banana Shower Count", Name = @"Banana Shower Count",
Content = bananaShowers.ToString(), Content = bananaShowers.ToString(),
Icon = FontAwesome.Regular.Circle CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
} }
}; };
} }

View File

@ -5,6 +5,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Framework.Extensions.IEnumerableExtensions; 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); public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap) protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken)
{ {
var positionData = obj as IHasXPosition; var positionData = obj as IHasXPosition;
var comboData = obj as IHasCombo; var comboData = obj as IHasCombo;

View File

@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
if (amount > 0) if (amount > 0)
{ {
// Clamp to the right bound // Clamp to the right bound
if (position + amount < 1) if (position + amount < CatchPlayfield.WIDTH)
position += amount; position += amount;
} }
else else
@ -212,6 +212,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2; 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; int lastDirection = 0;
double lastExcess = halfCatcherWidth; double lastExcess = halfCatcherWidth;

View File

@ -21,13 +21,11 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using System; using System;
using osu.Framework.Testing;
using osu.Game.Rulesets.Catch.Skinning; using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch namespace osu.Game.Rulesets.Catch
{ {
[ExcludeFromDynamicCompile]
public class CatchRuleset : Ruleset, ILegacyRuleset public class CatchRuleset : Ruleset, ILegacyRuleset
{ {
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableCatchRuleset(this, beatmap, mods); public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> 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 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; public int LegacyID => 2;

View File

@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Catch
Droplet, Droplet,
CatcherIdle, CatcherIdle,
CatcherFail, CatcherFail,
CatcherKiai CatcherKiai,
CatchComboCounter
} }
} }

View File

@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Catch.Difficulty
public class CatchDifficultyAttributes : DifficultyAttributes public class CatchDifficultyAttributes : DifficultyAttributes
{ {
public double ApproachRate; public double ApproachRate;
public int MaxCombo;
} }
} }

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -25,8 +24,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
private int tinyTicksMissed; private int tinyTicksMissed;
private int misses; private int misses;
public CatchPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score) public CatchPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
: base(ruleset, beatmap, score) : base(ruleset, attributes, score)
{ {
} }
@ -34,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{ {
mods = Score.Mods; mods = Score.Mods;
fruitsHit = Score.Statistics.GetOrDefault(HitResult.Perfect); fruitsHit = Score.Statistics.GetOrDefault(HitResult.Great);
ticksHit = Score.Statistics.GetOrDefault(HitResult.LargeTickHit); ticksHit = Score.Statistics.GetOrDefault(HitResult.LargeTickHit);
tinyTicksHit = Score.Statistics.GetOrDefault(HitResult.SmallTickHit); tinyTicksHit = Score.Statistics.GetOrDefault(HitResult.SmallTickHit);
tinyTicksMissed = Score.Statistics.GetOrDefault(HitResult.SmallTickMiss); tinyTicksMissed = Score.Statistics.GetOrDefault(HitResult.SmallTickMiss);

View File

@ -8,31 +8,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
{ {
public class CatchBananaJudgement : CatchJudgement public class CatchBananaJudgement : CatchJudgement
{ {
public override bool AffectsCombo => false; public override HitResult MaxResult => HitResult.LargeBonus;
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 bool ShouldExplodeFor(JudgementResult result) => true; public override bool ShouldExplodeFor(JudgementResult result) => true;
} }

View File

@ -7,16 +7,6 @@ namespace osu.Game.Rulesets.Catch.Judgements
{ {
public class CatchDropletJudgement : CatchJudgement public class CatchDropletJudgement : CatchJudgement
{ {
protected override int NumericResultFor(HitResult result) public override HitResult MaxResult => HitResult.LargeTickHit;
{
switch (result)
{
default:
return 0;
case HitResult.Perfect:
return 30;
}
}
} }
} }

View File

@ -9,19 +9,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
{ {
public class CatchJudgement : Judgement public class CatchJudgement : Judgement
{ {
public override HitResult MaxResult => HitResult.Perfect; public override HitResult MaxResult => HitResult.Great;
protected override int NumericResultFor(HitResult result)
{
switch (result)
{
default:
return 0;
case HitResult.Perfect:
return 300;
}
}
/// <summary> /// <summary>
/// Whether fruit on the platter should explode or drop. /// Whether fruit on the platter should explode or drop.

View File

@ -7,30 +7,6 @@ namespace osu.Game.Rulesets.Catch.Judgements
{ {
public class CatchTinyDropletJudgement : CatchJudgement public class CatchTinyDropletJudgement : CatchJudgement
{ {
public override bool AffectsCombo => false; public override HitResult MaxResult => HitResult.SmallTickHit;
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;
}
}
} }
} }

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Mods
protected override bool OnMouseMove(MouseMoveEvent e) 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); return base.OnMouseMove(e);
} }
} }

View File

@ -27,6 +27,11 @@ namespace osu.Game.Rulesets.Catch.Objects
set => x = value; set => x = value;
} }
/// <summary>
/// Whether this object can be placed on the catcher's plate.
/// </summary>
public virtual bool CanBePlated => false;
/// <summary> /// <summary>
/// A random offset applied to <see cref="X"/>, set by the <see cref="CatchBeatmapProcessor"/>. /// A random offset applied to <see cref="X"/>, set by the <see cref="CatchBeatmapProcessor"/>.
/// </summary> /// </summary>
@ -100,6 +105,14 @@ namespace osu.Game.Rulesets.Catch.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;
} }
/// <summary>
/// Represents a single object that can be caught by the catcher.
/// </summary>
public abstract class PalpableCatchHitObject : CatchHitObject
{
public override bool CanBePlated => true;
}
public enum FruitVisualRepresentation public enum FruitVisualRepresentation
{ {
Pear, Pear,

View File

@ -8,21 +8,18 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public abstract class PalpableCatchHitObject<TObject> : DrawableCatchHitObject<TObject> public abstract class PalpableDrawableCatchHitObject<TObject> : DrawableCatchHitObject<TObject>
where TObject : CatchHitObject where TObject : PalpableCatchHitObject
{ {
public override bool CanBePlated => true;
protected Container ScaleContainer { get; private set; } protected Container ScaleContainer { get; private set; }
protected PalpableCatchHitObject(TObject hitObject) protected PalpableDrawableCatchHitObject(TObject hitObject)
: base(hitObject) : base(hitObject)
{ {
Origin = Anchor.Centre; Origin = Anchor.Centre;
@ -65,9 +62,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject> public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject>
{ {
public virtual bool CanBePlated => false; public virtual bool StaysOnPlate => HitObject.CanBePlated;
public virtual bool StaysOnPlate => CanBePlated;
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; 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 (CheckPosition == null) return;
if (timeOffset >= 0 && Result != null) 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) protected override void UpdateStateTransforms(ArmedState state)

View File

@ -4,12 +4,11 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableDroplet : PalpableCatchHitObject<Droplet> public class DrawableDroplet : PalpableDrawableCatchHitObject<Droplet>
{ {
public override bool StaysOnPlate => false; public override bool StaysOnPlate => false;
@ -21,11 +20,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
ScaleContainer.Child = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new Pulp ScaleContainer.Child = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new DropletPiece());
{
Size = Size / 4,
AccentColour = { BindTarget = AccentColour }
});
} }
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()

View File

@ -8,7 +8,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableFruit : PalpableCatchHitObject<Fruit> public class DrawableFruit : PalpableDrawableCatchHitObject<Fruit>
{ {
public DrawableFruit(Fruit h) public DrawableFruit(Fruit h)
: base(h) : base(h)

View File

@ -0,0 +1,69 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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,
}
}
}
}
});
}
}
}
}

View File

@ -3,7 +3,6 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -21,11 +20,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public const float RADIUS_ADJUST = 1.1f; public const float RADIUS_ADJUST = 1.1f;
private Circle border; private Circle border;
private CatchHitObject hitObject; private CatchHitObject hitObject;
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
public FruitPiece() public FruitPiece()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -37,8 +33,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject;
hitObject = drawableCatchObject.HitObject; hitObject = drawableCatchObject.HitObject;
accentColour.BindTo(drawableCatchObject.AccentColour);
AddRangeInternal(new[] AddRangeInternal(new[]
{ {
getFruitFor(drawableCatchObject.HitObject.VisualRepresentation), getFruitFor(drawableCatchObject.HitObject.VisualRepresentation),

View File

@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Glow, Type = EdgeEffectType.Glow,
Radius = Size.X / 2, Radius = DrawWidth / 2,
Colour = colour.NewValue.Darken(0.2f).Opacity(0.75f) Colour = colour.NewValue.Darken(0.2f).Opacity(0.75f)
}; };
} }

View File

@ -6,7 +6,7 @@ using osu.Game.Rulesets.Judgements;
namespace osu.Game.Rulesets.Catch.Objects namespace osu.Game.Rulesets.Catch.Objects
{ {
public class Droplet : CatchHitObject public class Droplet : PalpableCatchHitObject
{ {
public override Judgement CreateJudgement() => new CatchDropletJudgement(); public override Judgement CreateJudgement() => new CatchDropletJudgement();
} }

View File

@ -6,7 +6,7 @@ using osu.Game.Rulesets.Judgements;
namespace osu.Game.Rulesets.Catch.Objects namespace osu.Game.Rulesets.Catch.Objects
{ {
public class Fruit : CatchHitObject public class Fruit : PalpableCatchHitObject
{ {
public override Judgement CreateJudgement() => new CatchJudgement(); public override Judgement CreateJudgement() => new CatchJudgement();
} }

View File

@ -31,6 +31,9 @@ namespace osu.Game.Rulesets.Catch.Replays
public override Replay Generate() public override Replay Generate()
{ {
if (Beatmap.HitObjects.Count == 0)
return Replay;
// todo: add support for HT DT // todo: add support for HT DT
const double dash_speed = Catcher.BASE_SPEED; const double dash_speed = Catcher.BASE_SPEED;
const double movement_speed = dash_speed / 2; const double movement_speed = dash_speed / 2;

View File

@ -0,0 +1,17 @@
{
"Mappings": [{
"StartTime": 3368,
"Objects": [{
"StartTime": 3368,
"Position": 374
}]
},
{
"StartTime": 3501,
"Objects": [{
"StartTime": 3501,
"Position": 446
}]
}
]
}

View File

@ -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:

View File

@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Catch.Scoring
{ {
switch (result) switch (result)
{ {
case HitResult.Perfect: case HitResult.Great:
case HitResult.Miss: case HitResult.Miss:
return true; return true;
} }

View File

@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Scoring
{ {
public class CatchScoreProcessor : ScoreProcessor public class CatchScoreProcessor : ScoreProcessor
{ {
public override HitWindows CreateHitWindows() => new CatchHitWindows();
} }
} }

View File

@ -6,6 +6,8 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics;
using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Catch.Skinning namespace osu.Game.Rulesets.Catch.Skinning
{ {
@ -51,6 +53,15 @@ namespace osu.Game.Rulesets.Catch.Skinning
case CatchSkinComponents.CatcherKiai: case CatchSkinComponents.CatcherKiai:
return this.GetAnimation("fruit-catcher-kiai", true, true, true) ?? return this.GetAnimation("fruit-catcher-kiai", true, true, true) ??
this.GetAnimation("fruit-ryuuta", true, true, true); this.GetAnimation("fruit-ryuuta", true, true, true);
case CatchSkinComponents.CatchComboCounter:
var comboFont = GetConfig<LegacySetting, string>(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; return null;
@ -61,7 +72,12 @@ namespace osu.Game.Rulesets.Catch.Skinning
switch (lookup) switch (lookup)
{ {
case CatchSkinColour colour: case CatchSkinColour colour:
return Source.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour)); var result = (Bindable<Color4>)Source.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));
if (result == null)
return null;
result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value);
return (IBindable<TValue>)result;
} }
return Source.GetConfig<TLookup, TValue>(lookup); return Source.GetConfig<TLookup, TValue>(lookup);

View File

@ -0,0 +1,103 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
/// </summary>
public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter
{
private readonly LegacyRollingCounter counter;
private readonly LegacyRollingCounter explosion;
public LegacyComboCounter(ISkin skin)
{
var fontName = skin.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
var fontOverlap = skin.GetConfig<LegacySetting, float>(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);
}
}
}
}

View File

@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Catch.Skinning
colouredSprite = new Sprite colouredSprite = new Sprite
{ {
Texture = skin.GetTexture(lookupName), Texture = skin.GetTexture(lookupName),
Colour = drawableObject.AccentColour.Value,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, },
@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Skinning
{ {
base.LoadComplete(); base.LoadComplete();
accentColour.BindValueChanged(colour => colouredSprite.Colour = colour.NewValue, true); accentColour.BindValueChanged(colour => colouredSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
} }
} }
} }

View File

@ -0,0 +1,62 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// Represents a component that displays a skinned <see cref="ICatchComboCounter"/> and handles combo judgement results for updating it accordingly.
/// </summary>
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);
}
}
}

View File

@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.UI
explodingFruitContainer, explodingFruitContainer,
CatcherArea.MovableCatcher.CreateProxiedContent(), CatcherArea.MovableCatcher.CreateProxiedContent(),
HitObjectContainer, HitObjectContainer,
CatcherArea CatcherArea,
}; };
} }
@ -62,6 +62,7 @@ namespace osu.Game.Rulesets.Catch.UI
public override void Add(DrawableHitObject h) public override void Add(DrawableHitObject h)
{ {
h.OnNewResult += onNewResult; h.OnNewResult += onNewResult;
h.OnRevertResult += onRevertResult;
base.Add(h); base.Add(h);
@ -70,6 +71,9 @@ namespace osu.Game.Rulesets.Catch.UI
} }
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) 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);
} }
} }

View File

@ -10,15 +10,21 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
public class CatchPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer public class CatchPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{ {
private const float playfield_size_adjust = 0.8f;
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => content;
private readonly Container content; private readonly Container content;
public CatchPlayfieldAdjustmentContainer() public CatchPlayfieldAdjustmentContainer()
{ {
Anchor = Anchor.TopCentre; // because we are using centre anchor/origin, we will need to limit visibility in the future
Origin = Anchor.TopCentre; // 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 InternalChild = new Container
{ {
@ -27,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.UI
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit, FillMode = FillMode.Fit,
FillAspectRatio = 4f / 3, 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(); 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); 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);
} }
} }
} }

View File

@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// <summary> /// <summary>
/// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable. /// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable.
/// </summary> /// </summary>
private const float allowed_catch_range = 0.8f; public const float ALLOWED_CATCH_RANGE = 0.8f;
/// <summary> /// <summary>
/// The drawable catcher for <see cref="CurrentState"/>. /// The drawable catcher for <see cref="CurrentState"/>.
@ -166,7 +166,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary> /// </summary>
/// <param name="scale">The scale of the catcher.</param> /// <param name="scale">The scale of the catcher.</param>
internal static float CalculateCatchWidth(Vector2 scale) 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;
/// <summary> /// <summary>
/// Calculates the width of the area used for attempting catches in gameplay. /// Calculates the width of the area used for attempting catches in gameplay.
@ -216,6 +216,9 @@ namespace osu.Game.Rulesets.Catch.UI
/// <returns>Whether the catch is possible.</returns> /// <returns>Whether the catch is possible.</returns>
public bool AttemptCatch(CatchHitObject fruit) public bool AttemptCatch(CatchHitObject fruit)
{ {
if (!fruit.CanBePlated)
return false;
var halfCatchWidth = catchWidth * 0.5f; var halfCatchWidth = catchWidth * 0.5f;
// this stuff wil disappear once we move fruit to non-relative coordinate space in the future. // 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 &&
catchObjectPosition <= catcherPosition + halfCatchWidth; catchObjectPosition <= catcherPosition + halfCatchWidth;
// only update hyperdash state if we are catching a fruit. // only update hyperdash state if we are not catching a tiny droplet.
// exceptions are Droplets and JuiceStreams. if (fruit is TinyDroplet) return validCatch;
if (!(fruit is Fruit)) return validCatch;
if (validCatch && fruit.HyperDash) if (validCatch && fruit.HyperDash)
{ {
@ -283,8 +285,6 @@ namespace osu.Game.Rulesets.Catch.UI
private void runHyperDashStateTransition(bool hyperDashing) private void runHyperDashStateTransition(bool hyperDashing)
{ {
trails.HyperDashTrailsColour = hyperDashColour;
trails.EndGlowSpritesColour = hyperDashEndGlowColour;
updateTrailVisibility(); updateTrailVisibility();
if (hyperDashing) if (hyperDashing)
@ -401,6 +401,9 @@ namespace osu.Game.Rulesets.Catch.UI
skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value ?? skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value ??
hyperDashColour; hyperDashColour;
trails.HyperDashTrailsColour = hyperDashColour;
trails.EndGlowSpritesColour = hyperDashEndGlowColour;
runHyperDashStateTransition(HyperDashing); runHyperDashStateTransition(HyperDashing);
} }

View File

@ -11,6 +11,7 @@ using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osuTK; using osuTK;
@ -23,6 +24,7 @@ namespace osu.Game.Rulesets.Catch.UI
public Func<CatchHitObject, DrawableHitObject<CatchHitObject>> CreateDrawableRepresentation; public Func<CatchHitObject, DrawableHitObject<CatchHitObject>> CreateDrawableRepresentation;
public readonly Catcher MovableCatcher; public readonly Catcher MovableCatcher;
private readonly CatchComboDisplay comboDisplay;
public Container ExplodingFruitTarget public Container ExplodingFruitTarget
{ {
@ -34,12 +36,24 @@ namespace osu.Game.Rulesets.Catch.UI
public CatcherArea(BeatmapDifficulty difficulty = null) public CatcherArea(BeatmapDifficulty difficulty = null)
{ {
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); 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; return;
void runAfterLoaded(Action action) void runAfterLoaded(Action action)
@ -55,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.UI
lastPlateableFruit.OnLoadComplete += _ => action(); 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. // create a new (cloned) fruit to stay on the plate. the original is faded out immediately.
var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit.HitObject); var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit.HitObject);
@ -86,8 +100,13 @@ namespace osu.Game.Rulesets.Catch.UI
else else
MovableCatcher.Drop(); MovableCatcher.Drop();
} }
comboDisplay.OnNewResult(fruit, result);
} }
public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result)
=> comboDisplay.OnRevertResult(fruit, result);
public void OnReleased(CatchAction action) public void OnReleased(CatchAction action)
{ {
} }
@ -105,6 +124,8 @@ namespace osu.Game.Rulesets.Catch.UI
if (state?.CatcherX != null) if (state?.CatcherX != null)
MovableCatcher.X = state.CatcherX.Value; MovableCatcher.X = state.CatcherX.Value;
comboDisplay.X = MovableCatcher.X;
} }
} }
} }

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.UI
private readonly Container<CatcherTrailSprite> hyperDashTrails; private readonly Container<CatcherTrailSprite> hyperDashTrails;
private readonly Container<CatcherTrailSprite> endGlowSprites; private readonly Container<CatcherTrailSprite> endGlowSprites;
private Color4 hyperDashTrailsColour; private Color4 hyperDashTrailsColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
public Color4 HyperDashTrailsColour public Color4 HyperDashTrailsColour
{ {
@ -35,11 +35,11 @@ namespace osu.Game.Rulesets.Catch.UI
return; return;
hyperDashTrailsColour = value; 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 public Color4 EndGlowSpritesColour
{ {
@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.UI
return; return;
endGlowSpritesColour = value; endGlowSpritesColour = value;
endGlowSprites.FadeColour(endGlowSpritesColour, Catcher.HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); endGlowSprites.Colour = endGlowSpritesColour;
} }
} }

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// An interface providing a set of methods to update the combo counter.
/// </summary>
public interface ICatchComboCounter : IDrawable
{
/// <summary>
/// Updates the counter to animate a transition from the old combo value it had to the current provided one.
/// </summary>
/// <remarks>
/// This is called regardless of whether the clock is rewinding.
/// </remarks>
/// <param name="combo">The new combo value.</param>
/// <param name="hitObjectColour">The colour of the object if hit, null on miss.</param>
void UpdateCombo(int combo, Color4? hitObjectColour = null);
}
}

View File

@ -16,7 +16,7 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public abstract class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene public abstract class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene
{ {

View File

@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene
{ {

View File

@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
[TestFixture] [TestFixture]
public class TestSceneEditor : EditorTestScene public class TestSceneEditor : EditorTestScene

View File

@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene public class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
{ {

View File

@ -12,7 +12,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public class TestSceneHoldNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene public class TestSceneHoldNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
{ {

View File

@ -20,7 +20,7 @@ using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public class TestSceneManiaBeatSnapGrid : EditorClockTestScene public class TestSceneManiaBeatSnapGrid : EditorClockTestScene
{ {

View File

@ -23,7 +23,7 @@ using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public class TestSceneManiaHitObjectComposer : EditorClockTestScene public class TestSceneManiaHitObjectComposer : EditorClockTestScene
{ {

View File

@ -18,7 +18,7 @@ using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public class TestSceneNotePlacementBlueprint : ManiaPlacementBlueprintTestScene public class TestSceneNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
{ {

View File

@ -12,7 +12,7 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
{ {

View File

@ -14,11 +14,13 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests
{ {
[TestFixture] [TestFixture]
[Timeout(10000)]
public class ManiaBeatmapConversionTest : BeatmapConversionTest<ManiaConvertMapping, ConvertValue> public class ManiaBeatmapConversionTest : BeatmapConversionTest<ManiaConvertMapping, ConvertValue>
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase("basic")] [TestCase("basic")]
[TestCase("zero-length-slider")]
public void Test(string name) => base.Test(name); public void Test(string name) => base.Test(name);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject) protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)

View File

@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime), 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.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); public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods);
protected override Ruleset CreateRuleset() => new ManiaRuleset(); protected override Ruleset CreateRuleset() => new ManiaRuleset();

View File

@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
});
}
}

View File

@ -22,18 +22,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[Cached] [Cached]
private readonly Column column; private readonly Column column;
public ColumnTestContainer(int column, ManiaAction action) public ColumnTestContainer(int column, ManiaAction action, bool showColumn = false)
{
InternalChildren = new[]
{ {
this.column = new Column(column) this.column = new Column(column)
{ {
Action = { Value = action }, Action = { Value = action },
AccentColour = Color4.Orange, AccentColour = Color4.Orange,
ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd,
}; Alpha = showColumn ? 1 : 0
},
InternalChild = content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
},
this.column.TopLevelContainer.CreateProxy()
}; };
} }
} }

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Children = new Drawable[] Children = new Drawable[]
{ {
new ColumnTestContainer(0, ManiaAction.Key1) new ColumnTestContainer(0, ManiaAction.Key1, true)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = 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, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq; using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -13,7 +14,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
public class TestSceneHoldNote : ManiaHitObjectTestScene public class TestSceneHoldNote : ManiaHitObjectTestScene
{ {
public TestSceneHoldNote() [Test]
public void TestHoldNote()
{ {
AddToggleStep("toggle hitting", v => AddToggleStep("toggle hitting", v =>
{ {

View File

@ -3,6 +3,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -13,7 +14,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() 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, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -3,6 +3,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Tests.Skinning namespace osu.Game.Rulesets.Mania.Tests.Skinning
@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() 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, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -45,9 +45,9 @@ namespace osu.Game.Rulesets.Mania.Tests
}); });
assertHeadJudgement(HitResult.Miss); assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.Miss); assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss); assertTailJudgement(HitResult.Miss);
assertNoteJudgement(HitResult.Perfect); assertNoteJudgement(HitResult.IgnoreHit);
} }
/// <summary> /// <summary>
@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}); });
assertHeadJudgement(HitResult.Miss); assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.Miss); assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss); assertTailJudgement(HitResult.Miss);
} }
@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}); });
assertHeadJudgement(HitResult.Miss); assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.Miss); assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss); assertTailJudgement(HitResult.Miss);
} }
@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}); });
assertHeadJudgement(HitResult.Perfect); assertHeadJudgement(HitResult.Perfect);
assertTickJudgement(HitResult.Perfect); assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Miss); assertTailJudgement(HitResult.Miss);
} }
@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}); });
assertHeadJudgement(HitResult.Perfect); assertHeadJudgement(HitResult.Perfect);
assertTickJudgement(HitResult.Perfect); assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Perfect); assertTailJudgement(HitResult.Perfect);
} }
@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}); });
assertHeadJudgement(HitResult.Perfect); assertHeadJudgement(HitResult.Perfect);
assertTickJudgement(HitResult.Miss); assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss); assertTailJudgement(HitResult.Miss);
} }
@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}); });
assertHeadJudgement(HitResult.Perfect); assertHeadJudgement(HitResult.Perfect);
assertTickJudgement(HitResult.Perfect); assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Miss); assertTailJudgement(HitResult.Miss);
} }
@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}); });
assertHeadJudgement(HitResult.Perfect); assertHeadJudgement(HitResult.Perfect);
assertTickJudgement(HitResult.Perfect); assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Meh); assertTailJudgement(HitResult.Meh);
} }
@ -199,7 +199,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}); });
assertHeadJudgement(HitResult.Miss); assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.Perfect); assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Miss); assertTailJudgement(HitResult.Miss);
} }
@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}); });
assertHeadJudgement(HitResult.Miss); assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.Perfect); assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Meh); assertTailJudgement(HitResult.Meh);
} }
@ -235,7 +235,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}); });
assertHeadJudgement(HitResult.Miss); assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.Miss); assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Meh); assertTailJudgement(HitResult.Meh);
} }
@ -280,10 +280,10 @@ namespace osu.Game.Rulesets.Mania.Tests
}, beatmap); }, beatmap);
AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject)) 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)) 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) private void assertHeadJudgement(HitResult result)

View File

@ -28,8 +28,15 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestFixture] [TestFixture]
public class TestSceneNotes : OsuTestScene public class TestSceneNotes : OsuTestScene
{ {
[BackgroundDependencyLoader] [Test]
private void load() public void TestVariousNotes()
{
DrawableNote note1 = null;
DrawableNote note2 = null;
DrawableHoldNote holdNote1 = null;
DrawableHoldNote holdNote2 = null;
AddStep("create notes", () =>
{ {
Child = new FillFlowContainer Child = new FillFlowContainer
{ {
@ -41,12 +48,13 @@ namespace osu.Game.Rulesets.Mania.Tests
Spacing = new Vector2(20), Spacing = new Vector2(20),
Children = new[] Children = new[]
{ {
createNoteDisplay(ScrollingDirection.Down, 1, out var note1), createNoteDisplay(ScrollingDirection.Down, 1, out note1),
createNoteDisplay(ScrollingDirection.Up, 2, out var note2), createNoteDisplay(ScrollingDirection.Up, 2, out note2),
createHoldNoteDisplay(ScrollingDirection.Down, 1, out var holdNote1), createHoldNoteDisplay(ScrollingDirection.Down, 1, out holdNote1),
createHoldNoteDisplay(ScrollingDirection.Up, 2, out var holdNote2), createHoldNoteDisplay(ScrollingDirection.Up, 2, out holdNote2),
} }
}; };
});
AddAssert("note 1 facing downwards", () => verifyAnchors(note1, Anchor.y2)); AddAssert("note 1 facing downwards", () => verifyAnchors(note1, Anchor.y2));
AddAssert("note 2 facing upwards", () => verifyAnchors(note2, Anchor.y0)); AddAssert("note 2 facing upwards", () => verifyAnchors(note2, Anchor.y0));

View File

@ -0,0 +1,117 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<ManiaHitObject>();
var frames = new List<ReplayFrame>();
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<JudgementResult> judgementResults;
private void performTest(List<ManiaHitObject> hitObjects, List<ReplayFrame> 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<JudgementResult>();
});
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)
{
}
}
}
}

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
@ -41,14 +40,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Note Count", Name = @"Note Count",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
Content = notes.ToString(), Content = notes.ToString(),
Icon = FontAwesome.Regular.Circle
}, },
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Hold Note Count", Name = @"Hold Note Count",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
Content = holdnotes.ToString(), Content = holdnotes.ToString(),
Icon = FontAwesome.Regular.Circle
}, },
}; };
} }

View File

@ -5,7 +5,7 @@ using osu.Game.Rulesets.Mania.Objects;
using System; using System;
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Utils; using System.Threading;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects; 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); public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original) protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{ {
BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty; 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); int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
Random = new FastRandom(seed); Random = new FastRandom(seed);
return base.ConvertBeatmap(original); return base.ConvertBeatmap(original, cancellationToken);
} }
protected override Beatmap<ManiaHitObject> CreateBeatmap() protected override Beatmap<ManiaHitObject> CreateBeatmap()
@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
return beatmap; return beatmap;
} }
protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap) protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
{ {
if (original is ManiaHitObject maniaOriginal) if (original is ManiaHitObject maniaOriginal)
{ {
@ -167,8 +167,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
var positionData = original as IHasPosition; 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); recordNote(time, positionData?.Position ?? Vector2.Zero);
computeDensity(time); computeDensity(time);
} }

View File

@ -27,8 +27,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
public readonly double EndTime; public readonly double EndTime;
public readonly double SegmentDuration; public readonly double SegmentDuration;
public readonly int SpanCount;
private readonly int spanCount;
private PatternType convertType; private PatternType convertType;
@ -42,20 +41,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var distanceData = hitObject as IHasDistance; var distanceData = hitObject as IHasDistance;
var repeatsData = hitObject as IHasRepeats; var repeatsData = hitObject as IHasRepeats;
spanCount = repeatsData?.SpanCount() ?? 1; SpanCount = repeatsData?.SpanCount() ?? 1;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime); DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
// The true distance, accounting for any repeats // 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 // 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; double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength;
// The duration of the osu! hit object // The duration of the osu! hit object
double osuDuration = distance / osuVelocity; double osuDuration = distance / osuVelocity;
EndTime = hitObject.StartTime + osuDuration; EndTime = hitObject.StartTime + osuDuration;
SegmentDuration = (EndTime - HitObject.StartTime) / spanCount; SegmentDuration = (EndTime - HitObject.StartTime) / SpanCount;
} }
public override IEnumerable<Pattern> Generate() public override IEnumerable<Pattern> Generate()
@ -96,7 +95,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern; return pattern;
} }
if (spanCount > 1) if (SpanCount > 1)
{ {
if (SegmentDuration <= 90) if (SegmentDuration <= 90)
return generateRandomHoldNotes(HitObject.StartTime, 1); return generateRandomHoldNotes(HitObject.StartTime, 1);
@ -104,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (SegmentDuration <= 120) if (SegmentDuration <= 120)
{ {
convertType |= PatternType.ForceNotStack; convertType |= PatternType.ForceNotStack;
return generateRandomNotes(HitObject.StartTime, spanCount + 1); return generateRandomNotes(HitObject.StartTime, SpanCount + 1);
} }
if (SegmentDuration <= 160) if (SegmentDuration <= 160)
@ -117,7 +116,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (duration >= 4000) if (duration >= 4000)
return generateNRandomNotes(HitObject.StartTime, 0.23, 0, 0); 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 generateTiledHoldNotes(HitObject.StartTime);
return generateHoldAndNormalNotes(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); int column = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
bool increasing = Random.NextDouble() > 0.5; 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); addToPattern(pattern, column, startTime, startTime);
startTime += SegmentDuration; startTime += SegmentDuration;
@ -302,7 +301,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); 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); addToPattern(pattern, nextColumn, startTime, startTime);
@ -393,7 +392,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern(); 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); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
@ -447,7 +446,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var rowPattern = new Pattern(); var rowPattern = new Pattern();
for (int i = 0; i <= spanCount; i++) for (int i = 0; i <= SpanCount; i++)
{ {
if (!(ignoreHead && startTime == HitObject.StartTime)) if (!(ignoreHead && startTime == HitObject.StartTime))
{ {

View File

@ -21,14 +21,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// </summary> /// </summary>
/// <param name="column">The 0-based column index.</param> /// <param name="column">The 0-based column index.</param>
/// <returns>Whether the column is a special column.</returns> /// <returns>Whether the column is a special column.</returns>
public bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2; public readonly bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2;
/// <summary> /// <summary>
/// Get the type of column given a column index. /// Get the type of column given a column index.
/// </summary> /// </summary>
/// <param name="column">The 0-based column index.</param> /// <param name="column">The 0-based column index.</param>
/// <returns>The type of the column.</returns> /// <returns>The type of the column.</returns>
public ColumnType GetTypeOfColumn(int column) public readonly ColumnType GetTypeOfColumn(int column)
{ {
if (IsSpecialColumn(column)) if (IsSpecialColumn(column))
return ColumnType.Special; return ColumnType.Special;

Some files were not shown because too many files have changed in this diff Show More