mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 09:07:52 +08:00
Merge branch 'master' into morth-taiko-changes
This commit is contained in:
commit
d387c0cba0
@ -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
|
@ -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>
|
||||||
|
73
Gemfile.lock
73
Gemfile.lock
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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.812.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.810.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.903.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -25,6 +25,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)
|
||||||
|
84
osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs
Normal file
84
osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 }
|
||||||
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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.7.0" />
|
<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" />
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -15,14 +15,12 @@ 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 +63,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;
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
69
osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs
Normal file
69
osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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),
|
||||||
|
@ -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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"Mappings": [{
|
||||||
|
"StartTime": 3368,
|
||||||
|
"Objects": [{
|
||||||
|
"StartTime": 3368,
|
||||||
|
"Position": 374
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"StartTime": 3501,
|
||||||
|
"Objects": [{
|
||||||
|
"StartTime": 3501,
|
||||||
|
"Position": 446
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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:
|
@ -6,6 +6,7 @@ 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;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Skinning
|
namespace osu.Game.Rulesets.Catch.Skinning
|
||||||
{
|
{
|
||||||
@ -61,7 +62,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);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +55,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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
117
osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
Normal file
117
osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
Normal 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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.7.0" />
|
<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" />
|
||||||
|
@ -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;
|
||||||
|
@ -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 osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
|
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
|
||||||
|
|
||||||
@ -15,7 +16,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
|
|||||||
AccentColour.Value = colours.Yellow;
|
AccentColour.Value = colours.Yellow;
|
||||||
|
|
||||||
Background.Alpha = 0.5f;
|
Background.Alpha = 0.5f;
|
||||||
Foreground.Alpha = 0;
|
}
|
||||||
}
|
|
||||||
|
protected override Drawable CreateForeground() => base.CreateForeground().With(d => d.Alpha = 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ namespace osu.Game.Rulesets.Mania.Judgements
|
|||||||
{
|
{
|
||||||
public class HoldNoteTickJudgement : ManiaJudgement
|
public class HoldNoteTickJudgement : ManiaJudgement
|
||||||
{
|
{
|
||||||
protected override int NumericResultFor(HitResult result) => 20;
|
protected override int NumericResultFor(HitResult result) => result == MaxResult ? 20 : 0;
|
||||||
|
|
||||||
protected override double HealthIncreaseFor(HitResult result)
|
protected override double HealthIncreaseFor(HitResult result)
|
||||||
{
|
{
|
||||||
|
@ -126,6 +126,9 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
|
|
||||||
if (mods.HasFlag(LegacyMods.Random))
|
if (mods.HasFlag(LegacyMods.Random))
|
||||||
yield return new ManiaModRandom();
|
yield return new ManiaModRandom();
|
||||||
|
|
||||||
|
if (mods.HasFlag(LegacyMods.Mirror))
|
||||||
|
yield return new ManiaModMirror();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
|
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
|
||||||
@ -175,6 +178,10 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
case ManiaModFadeIn _:
|
case ManiaModFadeIn _:
|
||||||
value |= LegacyMods.FadeIn;
|
value |= LegacyMods.FadeIn;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ManiaModMirror _:
|
||||||
|
value |= LegacyMods.Mirror;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,6 +227,7 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
new ManiaModDualStages(),
|
new ManiaModDualStages(),
|
||||||
new ManiaModMirror(),
|
new ManiaModMirror(),
|
||||||
new ManiaModDifficultyAdjust(),
|
new ManiaModDifficultyAdjust(),
|
||||||
|
new ManiaModInvert(),
|
||||||
};
|
};
|
||||||
|
|
||||||
case ModType.Automation:
|
case ModType.Automation:
|
||||||
@ -325,6 +333,16 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
Height = 250
|
Height = 250
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
new StatisticRow
|
||||||
|
{
|
||||||
|
Columns = new[]
|
||||||
|
{
|
||||||
|
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
||||||
|
{
|
||||||
|
new UnstableRate(score.HitEvents)
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// 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 osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.UI;
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
@ -14,15 +15,23 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly int? TargetColumn;
|
public readonly int? TargetColumn;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The intended <see cref="StageDefinition"/> for this component.
|
||||||
|
/// May be null if the component is not a direct member of a <see cref="Stage"/>.
|
||||||
|
/// </summary>
|
||||||
|
public readonly StageDefinition? StageDefinition;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="ManiaSkinComponent"/>.
|
/// Creates a new <see cref="ManiaSkinComponent"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="component">The component.</param>
|
/// <param name="component">The component.</param>
|
||||||
/// <param name="targetColumn">The intended <see cref="Column"/> index for this component. May be null if the component does not exist in a <see cref="Column"/>.</param>
|
/// <param name="targetColumn">The intended <see cref="Column"/> index for this component. May be null if the component does not exist in a <see cref="Column"/>.</param>
|
||||||
public ManiaSkinComponent(ManiaSkinComponents component, int? targetColumn = null)
|
/// <param name="stageDefinition">The intended <see cref="StageDefinition"/> for this component. May be null if the component is not a direct member of a <see cref="Stage"/>.</param>
|
||||||
|
public ManiaSkinComponent(ManiaSkinComponents component, int? targetColumn = null, StageDefinition? stageDefinition = null)
|
||||||
: base(component)
|
: base(component)
|
||||||
{
|
{
|
||||||
TargetColumn = targetColumn;
|
TargetColumn = targetColumn;
|
||||||
|
StageDefinition = stageDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME;
|
protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME;
|
||||||
|
76
osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs
Normal file
76
osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Audio;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Mods
|
||||||
|
{
|
||||||
|
public class ManiaModInvert : Mod, IApplicableAfterBeatmapConversion
|
||||||
|
{
|
||||||
|
public override string Name => "Invert";
|
||||||
|
|
||||||
|
public override string Acronym => "IN";
|
||||||
|
public override double ScoreMultiplier => 1;
|
||||||
|
|
||||||
|
public override string Description => "Hold the keys. To the beat.";
|
||||||
|
|
||||||
|
public override IconUsage? Icon => FontAwesome.Solid.YinYang;
|
||||||
|
|
||||||
|
public override ModType Type => ModType.Conversion;
|
||||||
|
|
||||||
|
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||||
|
{
|
||||||
|
var maniaBeatmap = (ManiaBeatmap)beatmap;
|
||||||
|
|
||||||
|
var newObjects = new List<ManiaHitObject>();
|
||||||
|
|
||||||
|
foreach (var column in maniaBeatmap.HitObjects.GroupBy(h => h.Column))
|
||||||
|
{
|
||||||
|
var newColumnObjects = new List<ManiaHitObject>();
|
||||||
|
|
||||||
|
var locations = column.OfType<Note>().Select(n => (startTime: n.StartTime, samples: n.Samples))
|
||||||
|
.Concat(column.OfType<HoldNote>().SelectMany(h => new[]
|
||||||
|
{
|
||||||
|
(startTime: h.StartTime, samples: h.GetNodeSamples(0)),
|
||||||
|
(startTime: h.EndTime, samples: h.GetNodeSamples(1))
|
||||||
|
}))
|
||||||
|
.OrderBy(h => h.startTime).ToList();
|
||||||
|
|
||||||
|
for (int i = 0; i < locations.Count - 1; i++)
|
||||||
|
{
|
||||||
|
// Full duration of the hold note.
|
||||||
|
double duration = locations[i + 1].startTime - locations[i].startTime;
|
||||||
|
|
||||||
|
// Beat length at the end of the hold note.
|
||||||
|
double beatLength = beatmap.ControlPointInfo.TimingPointAt(locations[i + 1].startTime).BeatLength;
|
||||||
|
|
||||||
|
// Decrease the duration by at most a 1/4 beat to ensure there's no instantaneous notes.
|
||||||
|
duration = Math.Max(duration / 2, duration - beatLength / 4);
|
||||||
|
|
||||||
|
newColumnObjects.Add(new HoldNote
|
||||||
|
{
|
||||||
|
Column = column.Key,
|
||||||
|
StartTime = locations[i].startTime,
|
||||||
|
Duration = duration,
|
||||||
|
NodeSamples = new List<IList<HitSampleInfo>> { locations[i].samples, Array.Empty<HitSampleInfo>() }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
newObjects.AddRange(newColumnObjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
maniaBeatmap.HitObjects = newObjects.OrderBy(h => h.StartTime).ToList();
|
||||||
|
|
||||||
|
// No breaks
|
||||||
|
maniaBeatmap.Breaks.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
// 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;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
|
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
|
||||||
@ -32,7 +33,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
private readonly Container<DrawableHoldNoteTail> tailContainer;
|
private readonly Container<DrawableHoldNoteTail> tailContainer;
|
||||||
private readonly Container<DrawableHoldNoteTick> tickContainer;
|
private readonly Container<DrawableHoldNoteTick> tickContainer;
|
||||||
|
|
||||||
private readonly Drawable bodyPiece;
|
/// <summary>
|
||||||
|
/// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Container sizingContainer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains the contents of the hold note that should be masked as the hold note is being pressed. Follows changes in the size of <see cref="sizingContainer"/>.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Container maskingContainer;
|
||||||
|
|
||||||
|
private readonly SkinnableDrawable bodyPiece;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
|
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
|
||||||
@ -44,24 +55,54 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasBroken { get; private set; }
|
public bool HasBroken { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the hold note has been released potentially without having caused a break.
|
||||||
|
/// </summary>
|
||||||
|
private double? releaseTime;
|
||||||
|
|
||||||
public DrawableHoldNote(HoldNote hitObject)
|
public DrawableHoldNote(HoldNote hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
|
|
||||||
AddRangeInternal(new[]
|
Container maskedContents;
|
||||||
|
|
||||||
|
AddRangeInternal(new Drawable[]
|
||||||
{
|
{
|
||||||
|
sizingContainer = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
maskingContainer = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = maskedContents = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Masking = true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headContainer = new Container<DrawableHoldNoteHead> { RelativeSizeAxes = Axes.Both }
|
||||||
|
}
|
||||||
|
},
|
||||||
bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece
|
bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both,
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X
|
RelativeSizeAxes = Axes.X
|
||||||
},
|
},
|
||||||
tickContainer = new Container<DrawableHoldNoteTick> { RelativeSizeAxes = Axes.Both },
|
tickContainer = new Container<DrawableHoldNoteTick> { RelativeSizeAxes = Axes.Both },
|
||||||
headContainer = new Container<DrawableHoldNoteHead> { RelativeSizeAxes = Axes.Both },
|
|
||||||
tailContainer = new Container<DrawableHoldNoteTail> { RelativeSizeAxes = Axes.Both },
|
tailContainer = new Container<DrawableHoldNoteTail> { RelativeSizeAxes = Axes.Both },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
maskedContents.AddRange(new[]
|
||||||
|
{
|
||||||
|
bodyPiece.CreateProxy(),
|
||||||
|
tickContainer.CreateProxy(),
|
||||||
|
tailContainer.CreateProxy(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||||
@ -127,7 +168,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
{
|
{
|
||||||
base.OnDirectionChanged(e);
|
base.OnDirectionChanged(e);
|
||||||
|
|
||||||
bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
|
if (e.NewValue == ScrollingDirection.Up)
|
||||||
|
{
|
||||||
|
bodyPiece.Anchor = bodyPiece.Origin = Anchor.TopLeft;
|
||||||
|
sizingContainer.Anchor = sizingContainer.Origin = Anchor.BottomLeft;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bodyPiece.Anchor = bodyPiece.Origin = Anchor.BottomLeft;
|
||||||
|
sizingContainer.Anchor = sizingContainer.Origin = Anchor.TopLeft;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void PlaySamples()
|
public override void PlaySamples()
|
||||||
@ -135,13 +185,48 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
// Samples are played by the head/tail notes.
|
// Samples are played by the head/tail notes.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void OnKilled()
|
||||||
|
{
|
||||||
|
base.OnKilled();
|
||||||
|
(bodyPiece.Drawable as IHoldNoteBody)?.Recycle();
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
// Make the body piece not lie under the head note
|
if (Time.Current < releaseTime)
|
||||||
|
releaseTime = null;
|
||||||
|
|
||||||
|
// Pad the full size container so its contents (i.e. the masking container) reach under the tail.
|
||||||
|
// This is required for the tail to not be masked away, since it lies outside the bounds of the hold note.
|
||||||
|
sizingContainer.Padding = new MarginPadding
|
||||||
|
{
|
||||||
|
Top = Direction.Value == ScrollingDirection.Down ? -Tail.Height : 0,
|
||||||
|
Bottom = Direction.Value == ScrollingDirection.Up ? -Tail.Height : 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pad the masking container to the starting position of the body piece (half-way under the head).
|
||||||
|
// This is required to make the body start getting masked immediately as soon as the note is held.
|
||||||
|
maskingContainer.Padding = new MarginPadding
|
||||||
|
{
|
||||||
|
Top = Direction.Value == ScrollingDirection.Up ? Head.Height / 2 : 0,
|
||||||
|
Bottom = Direction.Value == ScrollingDirection.Down ? Head.Height / 2 : 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Position and resize the body to lie half-way under the head and the tail notes.
|
||||||
bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2;
|
bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2;
|
||||||
bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2;
|
bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2;
|
||||||
|
|
||||||
|
// As the note is being held, adjust the size of the sizing container. This has two effects:
|
||||||
|
// 1. The contained masking container will mask the body and ticks.
|
||||||
|
// 2. The head note will move along with the new "head position" in the container.
|
||||||
|
if (Head.IsHit && releaseTime == null)
|
||||||
|
{
|
||||||
|
// How far past the hit target this hold note is. Always a positive value.
|
||||||
|
float yOffset = Math.Max(0, Direction.Value == ScrollingDirection.Up ? -Y : Y);
|
||||||
|
sizingContainer.Height = Math.Clamp(1 - yOffset / DrawHeight, 0, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateStateTransforms(ArmedState state)
|
protected override void UpdateStateTransforms(ArmedState state)
|
||||||
@ -153,7 +238,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
{
|
{
|
||||||
if (Tail.AllJudged)
|
if (Tail.AllJudged)
|
||||||
|
{
|
||||||
ApplyResult(r => r.Type = HitResult.Perfect);
|
ApplyResult(r => r.Type = HitResult.Perfect);
|
||||||
|
endHold();
|
||||||
|
}
|
||||||
|
|
||||||
if (Tail.Result.Type == HitResult.Miss)
|
if (Tail.Result.Type == HitResult.Miss)
|
||||||
HasBroken = true;
|
HasBroken = true;
|
||||||
@ -167,6 +255,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
if (action != Action.Value)
|
if (action != Action.Value)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (CheckHittable?.Invoke(this, Time.Current) == false)
|
||||||
|
return false;
|
||||||
|
|
||||||
// The tail has a lenience applied to it which is factored into the miss window (i.e. the miss judgement will be delayed).
|
// The tail has a lenience applied to it which is factored into the miss window (i.e. the miss judgement will be delayed).
|
||||||
// But the hold cannot ever be started within the late-lenience window, so we should skip trying to begin the hold during that time.
|
// But the hold cannot ever be started within the late-lenience window, so we should skip trying to begin the hold during that time.
|
||||||
// Note: Unlike below, we use the tail's start time to determine the time offset.
|
// Note: Unlike below, we use the tail's start time to determine the time offset.
|
||||||
@ -206,6 +297,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
// If the key has been released too early, the user should not receive full score for the release
|
// If the key has been released too early, the user should not receive full score for the release
|
||||||
if (!Tail.IsHit)
|
if (!Tail.IsHit)
|
||||||
HasBroken = true;
|
HasBroken = true;
|
||||||
|
|
||||||
|
releaseTime = Time.Current;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void endHold()
|
private void endHold()
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// 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 osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -17,6 +19,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
|
|
||||||
public void UpdateResult() => base.UpdateResult(true);
|
public void UpdateResult() => base.UpdateResult(true);
|
||||||
|
|
||||||
|
protected override void UpdateStateTransforms(ArmedState state)
|
||||||
|
{
|
||||||
|
// This hitobject should never expire, so this is just a safe maximum.
|
||||||
|
LifetimeEnd = LifetimeStart + 30000;
|
||||||
|
}
|
||||||
|
|
||||||
public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note
|
public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note
|
||||||
|
|
||||||
public override void OnReleased(ManiaAction action)
|
public override void OnReleased(ManiaAction action)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// 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;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -8,6 +9,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osu.Game.Rulesets.Mania.UI;
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||||
{
|
{
|
||||||
@ -34,6 +36,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this <see cref="DrawableManiaHitObject"/> can be hit, given a time value.
|
||||||
|
/// If non-null, judgements will be ignored whilst the function returns false.
|
||||||
|
/// </summary>
|
||||||
|
public Func<DrawableHitObject, double, bool> CheckHittable;
|
||||||
|
|
||||||
protected DrawableManiaHitObject(ManiaHitObject hitObject)
|
protected DrawableManiaHitObject(ManiaHitObject hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
{
|
{
|
||||||
@ -120,10 +128,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ArmedState.Hit:
|
case ArmedState.Hit:
|
||||||
this.FadeOut(150, Easing.OutQuint);
|
this.FadeOut();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Causes this <see cref="DrawableManiaHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
|
||||||
|
/// </summary>
|
||||||
|
public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class DrawableManiaHitObject<TObject> : DrawableManiaHitObject
|
public abstract class DrawableManiaHitObject<TObject> : DrawableManiaHitObject
|
||||||
|
@ -64,6 +64,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
if (action != Action.Value)
|
if (action != Action.Value)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (CheckHittable?.Invoke(this, Time.Current) == false)
|
||||||
|
return false;
|
||||||
|
|
||||||
return UpdateResult(true);
|
return UpdateResult(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,24 +19,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents length-wise portion of a hold note.
|
/// Represents length-wise portion of a hold note.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DefaultBodyPiece : CompositeDrawable
|
public class DefaultBodyPiece : CompositeDrawable, IHoldNoteBody
|
||||||
{
|
{
|
||||||
protected readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
|
protected readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
|
||||||
|
protected readonly IBindable<bool> IsHitting = new Bindable<bool>();
|
||||||
private readonly LayoutValue subtractionCache = new LayoutValue(Invalidation.DrawSize);
|
|
||||||
private readonly IBindable<bool> isHitting = new Bindable<bool>();
|
|
||||||
|
|
||||||
protected Drawable Background { get; private set; }
|
protected Drawable Background { get; private set; }
|
||||||
protected BufferedContainer Foreground { get; private set; }
|
private Container foregroundContainer;
|
||||||
|
|
||||||
private BufferedContainer subtractionContainer;
|
|
||||||
private Container subtractionLayer;
|
|
||||||
|
|
||||||
public DefaultBodyPiece()
|
public DefaultBodyPiece()
|
||||||
{
|
{
|
||||||
Blending = BlendingParameters.Additive;
|
Blending = BlendingParameters.Additive;
|
||||||
|
|
||||||
AddLayout(subtractionCache);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
@ -45,7 +38,54 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
|
|||||||
InternalChildren = new[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
Background = new Box { RelativeSizeAxes = Axes.Both },
|
Background = new Box { RelativeSizeAxes = Axes.Both },
|
||||||
Foreground = new BufferedContainer
|
foregroundContainer = new Container { RelativeSizeAxes = Axes.Both }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (drawableObject != null)
|
||||||
|
{
|
||||||
|
var holdNote = (DrawableHoldNote)drawableObject;
|
||||||
|
|
||||||
|
AccentColour.BindTo(drawableObject.AccentColour);
|
||||||
|
IsHitting.BindTo(holdNote.IsHitting);
|
||||||
|
}
|
||||||
|
|
||||||
|
AccentColour.BindValueChanged(onAccentChanged, true);
|
||||||
|
|
||||||
|
Recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Recycle() => foregroundContainer.Child = CreateForeground();
|
||||||
|
|
||||||
|
protected virtual Drawable CreateForeground() => new ForegroundPiece
|
||||||
|
{
|
||||||
|
AccentColour = { BindTarget = AccentColour },
|
||||||
|
IsHitting = { BindTarget = IsHitting }
|
||||||
|
};
|
||||||
|
|
||||||
|
private void onAccentChanged(ValueChangedEvent<Color4> accent) => Background.Colour = accent.NewValue.Opacity(0.7f);
|
||||||
|
|
||||||
|
private class ForegroundPiece : CompositeDrawable
|
||||||
|
{
|
||||||
|
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
|
||||||
|
public readonly IBindable<bool> IsHitting = new Bindable<bool>();
|
||||||
|
|
||||||
|
private readonly LayoutValue subtractionCache = new LayoutValue(Invalidation.DrawSize);
|
||||||
|
|
||||||
|
private BufferedContainer foregroundBuffer;
|
||||||
|
private BufferedContainer subtractionBuffer;
|
||||||
|
private Container subtractionLayer;
|
||||||
|
|
||||||
|
public ForegroundPiece()
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
AddLayout(subtractionCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
InternalChild = foregroundBuffer = new BufferedContainer
|
||||||
{
|
{
|
||||||
Blending = BlendingParameters.Additive,
|
Blending = BlendingParameters.Additive,
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
@ -53,7 +93,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
|
|||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new Box { RelativeSizeAxes = Axes.Both },
|
new Box { RelativeSizeAxes = Axes.Both },
|
||||||
subtractionContainer = new BufferedContainer
|
subtractionBuffer = new BufferedContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
// This is needed because we're blending with another object
|
// This is needed because we're blending with another object
|
||||||
@ -77,36 +117,26 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (drawableObject != null)
|
|
||||||
{
|
|
||||||
var holdNote = (DrawableHoldNote)drawableObject;
|
|
||||||
|
|
||||||
AccentColour.BindTo(drawableObject.AccentColour);
|
|
||||||
isHitting.BindTo(holdNote.IsHitting);
|
|
||||||
}
|
|
||||||
|
|
||||||
AccentColour.BindValueChanged(onAccentChanged, true);
|
AccentColour.BindValueChanged(onAccentChanged, true);
|
||||||
isHitting.BindValueChanged(_ => onAccentChanged(new ValueChangedEvent<Color4>(AccentColour.Value, AccentColour.Value)), true);
|
IsHitting.BindValueChanged(_ => onAccentChanged(new ValueChangedEvent<Color4>(AccentColour.Value, AccentColour.Value)), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onAccentChanged(ValueChangedEvent<Color4> accent)
|
private void onAccentChanged(ValueChangedEvent<Color4> accent)
|
||||||
{
|
{
|
||||||
Foreground.Colour = accent.NewValue.Opacity(0.5f);
|
foregroundBuffer.Colour = accent.NewValue.Opacity(0.5f);
|
||||||
Background.Colour = accent.NewValue.Opacity(0.7f);
|
|
||||||
|
|
||||||
const float animation_length = 50;
|
const float animation_length = 50;
|
||||||
|
|
||||||
Foreground.ClearTransforms(false, nameof(Foreground.Colour));
|
foregroundBuffer.ClearTransforms(false, nameof(foregroundBuffer.Colour));
|
||||||
|
|
||||||
if (isHitting.Value)
|
if (IsHitting.Value)
|
||||||
{
|
{
|
||||||
// wait for the next sync point
|
// wait for the next sync point
|
||||||
double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
|
double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
|
||||||
using (Foreground.BeginDelayedSequence(synchronisedOffset))
|
using (foregroundBuffer.BeginDelayedSequence(synchronisedOffset))
|
||||||
Foreground.FadeColour(accent.NewValue.Lighten(0.2f), animation_length).Then().FadeColour(Foreground.Colour, animation_length).Loop();
|
foregroundBuffer.FadeColour(accent.NewValue.Lighten(0.2f), animation_length).Then().FadeColour(foregroundBuffer.Colour, animation_length).Loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
subtractionCache.Invalidate();
|
subtractionCache.Invalidate();
|
||||||
@ -127,11 +157,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
|
|||||||
Radius = DrawWidth
|
Radius = DrawWidth
|
||||||
};
|
};
|
||||||
|
|
||||||
Foreground.ForceRedraw();
|
foregroundBuffer.ForceRedraw();
|
||||||
subtractionContainer.ForceRedraw();
|
subtractionBuffer.ForceRedraw();
|
||||||
|
|
||||||
subtractionCache.Validate();
|
subtractionCache.Validate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for mania hold note bodies.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHoldNoteBody
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Recycles the contents of this <see cref="IHoldNoteBody"/> to free used resources.
|
||||||
|
/// </summary>
|
||||||
|
void Recycle();
|
||||||
|
}
|
||||||
|
}
|
@ -102,14 +102,14 @@ namespace osu.Game.Rulesets.Mania.Objects
|
|||||||
{
|
{
|
||||||
StartTime = StartTime,
|
StartTime = StartTime,
|
||||||
Column = Column,
|
Column = Column,
|
||||||
Samples = getNodeSamples(0),
|
Samples = GetNodeSamples(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
AddNested(Tail = new TailNote
|
AddNested(Tail = new TailNote
|
||||||
{
|
{
|
||||||
StartTime = EndTime,
|
StartTime = EndTime,
|
||||||
Column = Column,
|
Column = Column,
|
||||||
Samples = getNodeSamples((NodeSamples?.Count - 1) ?? 1),
|
Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mania.Objects
|
|||||||
|
|
||||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||||
|
|
||||||
private IList<HitSampleInfo> getNodeSamples(int nodeIndex) =>
|
public IList<HitSampleInfo> GetNodeSamples(int nodeIndex) =>
|
||||||
nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
|
nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
46
osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs
Normal file
46
osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// 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.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Skinning
|
||||||
|
{
|
||||||
|
public class HitTargetInsetContainer : Container
|
||||||
|
{
|
||||||
|
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||||
|
|
||||||
|
protected override Container<Drawable> Content => content;
|
||||||
|
private readonly Container content;
|
||||||
|
|
||||||
|
private float hitPosition;
|
||||||
|
|
||||||
|
public HitTargetInsetContainer()
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
InternalChild = content = new Container { RelativeSizeAxes = Axes.Both };
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
|
||||||
|
{
|
||||||
|
hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? Stage.HIT_TARGET_POSITION;
|
||||||
|
|
||||||
|
direction.BindTo(scrollingInfo.Direction);
|
||||||
|
direction.BindValueChanged(onDirectionChanged, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||||
|
{
|
||||||
|
content.Padding = direction.NewValue == ScrollingDirection.Up
|
||||||
|
? new MarginPadding { Top = hitPosition }
|
||||||
|
: new MarginPadding { Bottom = hitPosition };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
// 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;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -19,7 +21,14 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
|||||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||||
private readonly IBindable<bool> isHitting = new Bindable<bool>();
|
private readonly IBindable<bool> isHitting = new Bindable<bool>();
|
||||||
|
|
||||||
private Drawable sprite;
|
[CanBeNull]
|
||||||
|
private Drawable bodySprite;
|
||||||
|
|
||||||
|
[CanBeNull]
|
||||||
|
private Drawable lightContainer;
|
||||||
|
|
||||||
|
[CanBeNull]
|
||||||
|
private Drawable light;
|
||||||
|
|
||||||
public LegacyBodyPiece()
|
public LegacyBodyPiece()
|
||||||
{
|
{
|
||||||
@ -32,7 +41,39 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
|||||||
string imageName = GetColumnSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
|
string imageName = GetColumnSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
|
||||||
?? $"mania-note{FallbackColumnIndex}L";
|
?? $"mania-note{FallbackColumnIndex}L";
|
||||||
|
|
||||||
sprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d =>
|
string lightImage = GetColumnSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightImage)?.Value
|
||||||
|
?? "lightingL";
|
||||||
|
|
||||||
|
float lightScale = GetColumnSkinConfig<float>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightScale)?.Value
|
||||||
|
?? 1;
|
||||||
|
|
||||||
|
// Create a temporary animation to retrieve the number of frames, in an effort to calculate the intended frame length.
|
||||||
|
// This animation is discarded and re-queried with the appropriate frame length afterwards.
|
||||||
|
var tmp = skin.GetAnimation(lightImage, true, false);
|
||||||
|
double frameLength = 0;
|
||||||
|
if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
|
||||||
|
frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount);
|
||||||
|
|
||||||
|
light = skin.GetAnimation(lightImage, true, true, frameLength: frameLength).With(d =>
|
||||||
|
{
|
||||||
|
if (d == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
d.Origin = Anchor.Centre;
|
||||||
|
d.Blending = BlendingParameters.Additive;
|
||||||
|
d.Scale = new Vector2(lightScale);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (light != null)
|
||||||
|
{
|
||||||
|
lightContainer = new HitTargetInsetContainer
|
||||||
|
{
|
||||||
|
Alpha = 0,
|
||||||
|
Child = light
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bodySprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d =>
|
||||||
{
|
{
|
||||||
if (d == null)
|
if (d == null)
|
||||||
return;
|
return;
|
||||||
@ -47,8 +88,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
|||||||
// Todo: Wrap
|
// Todo: Wrap
|
||||||
});
|
});
|
||||||
|
|
||||||
if (sprite != null)
|
if (bodySprite != null)
|
||||||
InternalChild = sprite;
|
InternalChild = bodySprite;
|
||||||
|
|
||||||
direction.BindTo(scrollingInfo.Direction);
|
direction.BindTo(scrollingInfo.Direction);
|
||||||
direction.BindValueChanged(onDirectionChanged, true);
|
direction.BindValueChanged(onDirectionChanged, true);
|
||||||
@ -60,28 +101,68 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
|||||||
|
|
||||||
private void onIsHittingChanged(ValueChangedEvent<bool> isHitting)
|
private void onIsHittingChanged(ValueChangedEvent<bool> isHitting)
|
||||||
{
|
{
|
||||||
if (!(sprite is TextureAnimation animation))
|
if (bodySprite is TextureAnimation bodyAnimation)
|
||||||
|
{
|
||||||
|
bodyAnimation.GotoFrame(0);
|
||||||
|
bodyAnimation.IsPlaying = isHitting.NewValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lightContainer == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
animation.GotoFrame(0);
|
if (isHitting.NewValue)
|
||||||
animation.IsPlaying = isHitting.NewValue;
|
{
|
||||||
|
// Clear the fade out and, more importantly, the removal.
|
||||||
|
lightContainer.ClearTransforms();
|
||||||
|
|
||||||
|
// Only add the container if the removal has taken place.
|
||||||
|
if (lightContainer.Parent == null)
|
||||||
|
Column.TopLevelContainer.Add(lightContainer);
|
||||||
|
|
||||||
|
// The light must be seeked only after being loaded, otherwise a nullref occurs (https://github.com/ppy/osu-framework/issues/3847).
|
||||||
|
if (light is TextureAnimation lightAnimation)
|
||||||
|
lightAnimation.GotoFrame(0);
|
||||||
|
|
||||||
|
lightContainer.FadeIn(80);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lightContainer.FadeOut(120)
|
||||||
|
.OnComplete(d => Column.TopLevelContainer.Remove(d));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||||
{
|
{
|
||||||
if (sprite == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (direction.NewValue == ScrollingDirection.Up)
|
if (direction.NewValue == ScrollingDirection.Up)
|
||||||
{
|
{
|
||||||
sprite.Origin = Anchor.BottomCentre;
|
if (bodySprite != null)
|
||||||
sprite.Scale = new Vector2(1, -1);
|
{
|
||||||
|
bodySprite.Origin = Anchor.BottomCentre;
|
||||||
|
bodySprite.Scale = new Vector2(1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (light != null)
|
||||||
|
light.Anchor = Anchor.TopCentre;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
sprite.Origin = Anchor.TopCentre;
|
if (bodySprite != null)
|
||||||
sprite.Scale = Vector2.One;
|
{
|
||||||
}
|
bodySprite.Origin = Anchor.TopCentre;
|
||||||
|
bodySprite.Scale = Vector2.One;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (light != null)
|
||||||
|
light.Anchor = Anchor.BottomCentre;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
lightContainer?.Expire();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
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.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
@ -18,14 +17,12 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
|||||||
public class LegacyColumnBackground : LegacyManiaColumnElement, IKeyBindingHandler<ManiaAction>
|
public class LegacyColumnBackground : LegacyManiaColumnElement, IKeyBindingHandler<ManiaAction>
|
||||||
{
|
{
|
||||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||||
private readonly bool isLastColumn;
|
|
||||||
|
|
||||||
private Container lightContainer;
|
private Container lightContainer;
|
||||||
private Sprite light;
|
private Sprite light;
|
||||||
|
|
||||||
public LegacyColumnBackground(bool isLastColumn)
|
public LegacyColumnBackground()
|
||||||
{
|
{
|
||||||
this.isLastColumn = isLastColumn;
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,52 +32,14 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
|||||||
string lightImage = skin.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.LightImage)?.Value
|
string lightImage = skin.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.LightImage)?.Value
|
||||||
?? "mania-stage-light";
|
?? "mania-stage-light";
|
||||||
|
|
||||||
float leftLineWidth = GetColumnSkinConfig<float>(skin, LegacyManiaSkinConfigurationLookups.LeftLineWidth)
|
|
||||||
?.Value ?? 1;
|
|
||||||
float rightLineWidth = GetColumnSkinConfig<float>(skin, LegacyManiaSkinConfigurationLookups.RightLineWidth)
|
|
||||||
?.Value ?? 1;
|
|
||||||
|
|
||||||
bool hasLeftLine = leftLineWidth > 0;
|
|
||||||
bool hasRightLine = rightLineWidth > 0 && skin.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m
|
|
||||||
|| isLastColumn;
|
|
||||||
|
|
||||||
float lightPosition = GetColumnSkinConfig<float>(skin, LegacyManiaSkinConfigurationLookups.LightPosition)?.Value
|
float lightPosition = GetColumnSkinConfig<float>(skin, LegacyManiaSkinConfigurationLookups.LightPosition)?.Value
|
||||||
?? 0;
|
?? 0;
|
||||||
|
|
||||||
Color4 lineColour = GetColumnSkinConfig<Color4>(skin, LegacyManiaSkinConfigurationLookups.ColumnLineColour)?.Value
|
|
||||||
?? Color4.White;
|
|
||||||
|
|
||||||
Color4 backgroundColour = GetColumnSkinConfig<Color4>(skin, LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour)?.Value
|
|
||||||
?? Color4.Black;
|
|
||||||
|
|
||||||
Color4 lightColour = GetColumnSkinConfig<Color4>(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value
|
Color4 lightColour = GetColumnSkinConfig<Color4>(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value
|
||||||
?? Color4.White;
|
?? Color4.White;
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Colour = backgroundColour
|
|
||||||
},
|
|
||||||
new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Y,
|
|
||||||
Width = leftLineWidth,
|
|
||||||
Scale = new Vector2(0.740f, 1),
|
|
||||||
Colour = lineColour,
|
|
||||||
Alpha = hasLeftLine ? 1 : 0
|
|
||||||
},
|
|
||||||
new Box
|
|
||||||
{
|
|
||||||
Anchor = Anchor.TopRight,
|
|
||||||
Origin = Anchor.TopRight,
|
|
||||||
RelativeSizeAxes = Axes.Y,
|
|
||||||
Width = rightLineWidth,
|
|
||||||
Scale = new Vector2(0.740f, 1),
|
|
||||||
Colour = lineColour,
|
|
||||||
Alpha = hasRightLine ? 1 : 0
|
|
||||||
},
|
|
||||||
lightContainer = new Container
|
lightContainer = new Container
|
||||||
{
|
{
|
||||||
Origin = Anchor.BottomCentre,
|
Origin = Anchor.BottomCentre,
|
||||||
@ -90,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.BottomCentre,
|
Anchor = Anchor.BottomCentre,
|
||||||
Origin = Anchor.BottomCentre,
|
Origin = Anchor.BottomCentre,
|
||||||
Colour = lightColour,
|
Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour),
|
||||||
Texture = skin.GetTexture(lightImage),
|
Texture = skin.GetTexture(lightImage),
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Width = 1,
|
Width = 1,
|
||||||
|
@ -7,6 +7,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Animations;
|
using osu.Framework.Graphics.Animations;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Mania.Judgements;
|
||||||
using osu.Game.Rulesets.Mania.UI;
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
@ -66,6 +67,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
|||||||
|
|
||||||
public void Animate(JudgementResult result)
|
public void Animate(JudgementResult result)
|
||||||
{
|
{
|
||||||
|
if (result.Judgement is HoldNoteTickJudgement)
|
||||||
|
return;
|
||||||
|
|
||||||
(explosion as IFramedAnimation)?.GotoFrame(0);
|
(explosion as IFramedAnimation)?.GotoFrame(0);
|
||||||
|
|
||||||
explosion?.FadeInFromZero(80)
|
explosion?.FadeInFromZero(80)
|
||||||
|
@ -20,11 +20,6 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
|||||||
|
|
||||||
private Container directionContainer;
|
private Container directionContainer;
|
||||||
|
|
||||||
public LegacyHitTarget()
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
|
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
|
||||||
{
|
{
|
||||||
@ -56,7 +51,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
|||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = 1,
|
Height = 1,
|
||||||
Colour = lineColour,
|
Colour = LegacyColourCompatibility.DisallowZeroAlpha(lineColour),
|
||||||
Alpha = showJudgementLine ? 0.9f : 0
|
Alpha = showJudgementLine ? 0.9f : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
|||||||
|
|
||||||
direction.BindTo(scrollingInfo.Direction);
|
direction.BindTo(scrollingInfo.Direction);
|
||||||
direction.BindValueChanged(onDirectionChanged, true);
|
direction.BindValueChanged(onDirectionChanged, true);
|
||||||
|
|
||||||
|
if (GetColumnSkinConfig<bool>(skin, LegacyManiaSkinConfigurationLookups.KeysUnderNotes)?.Value ?? false)
|
||||||
|
Column.UnderlayElements.Add(CreateProxy());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||||
|
@ -4,19 +4,27 @@
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
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.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Skinning
|
namespace osu.Game.Rulesets.Mania.Skinning
|
||||||
{
|
{
|
||||||
public class LegacyStageBackground : CompositeDrawable
|
public class LegacyStageBackground : CompositeDrawable
|
||||||
{
|
{
|
||||||
|
private readonly StageDefinition stageDefinition;
|
||||||
|
|
||||||
private Drawable leftSprite;
|
private Drawable leftSprite;
|
||||||
private Drawable rightSprite;
|
private Drawable rightSprite;
|
||||||
|
private ColumnFlow<Drawable> columnBackgrounds;
|
||||||
|
|
||||||
public LegacyStageBackground()
|
public LegacyStageBackground(StageDefinition stageDefinition)
|
||||||
{
|
{
|
||||||
|
this.stageDefinition = stageDefinition;
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,8 +52,19 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
|||||||
Origin = Anchor.TopLeft,
|
Origin = Anchor.TopLeft,
|
||||||
X = -0.05f,
|
X = -0.05f,
|
||||||
Texture = skin.GetTexture(rightImage)
|
Texture = skin.GetTexture(rightImage)
|
||||||
|
},
|
||||||
|
columnBackgrounds = new ColumnFlow<Drawable>(stageDefinition)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Y
|
||||||
|
},
|
||||||
|
new HitTargetInsetContainer
|
||||||
|
{
|
||||||
|
Child = new LegacyHitTarget { RelativeSizeAxes = Axes.Both }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < stageDefinition.Columns; i++)
|
||||||
|
columnBackgrounds.SetContentForColumn(i, new ColumnBackground(i, i == stageDefinition.Columns - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
@ -58,5 +77,72 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
|||||||
if (rightSprite?.Height > 0)
|
if (rightSprite?.Height > 0)
|
||||||
rightSprite.Scale = new Vector2(1, DrawHeight / rightSprite.Height);
|
rightSprite.Scale = new Vector2(1, DrawHeight / rightSprite.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ColumnBackground : CompositeDrawable
|
||||||
|
{
|
||||||
|
private readonly int columnIndex;
|
||||||
|
private readonly bool isLastColumn;
|
||||||
|
|
||||||
|
public ColumnBackground(int columnIndex, bool isLastColumn)
|
||||||
|
{
|
||||||
|
this.columnIndex = columnIndex;
|
||||||
|
this.isLastColumn = isLastColumn;
|
||||||
|
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(ISkinSource skin)
|
||||||
|
{
|
||||||
|
float leftLineWidth = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.LeftLineWidth, columnIndex)?.Value ?? 1;
|
||||||
|
float rightLineWidth = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.RightLineWidth, columnIndex)?.Value ?? 1;
|
||||||
|
|
||||||
|
bool hasLeftLine = leftLineWidth > 0;
|
||||||
|
bool hasRightLine = rightLineWidth > 0 && skin.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m
|
||||||
|
|| isLastColumn;
|
||||||
|
|
||||||
|
Color4 lineColour = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.ColumnLineColour, columnIndex)?.Value ?? Color4.White;
|
||||||
|
Color4 backgroundColour = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour, columnIndex)?.Value ?? Color4.Black;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
}, backgroundColour),
|
||||||
|
new HitTargetInsetContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new[]
|
||||||
|
{
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
Width = leftLineWidth,
|
||||||
|
Scale = new Vector2(0.740f, 1),
|
||||||
|
Alpha = hasLeftLine ? 1 : 0,
|
||||||
|
Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
}, lineColour)
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopRight,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
Width = rightLineWidth,
|
||||||
|
Scale = new Vector2(0.740f, 1),
|
||||||
|
Alpha = hasRightLine ? 1 : 0,
|
||||||
|
Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
}, lineColour)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using osu.Framework.Audio.Sample;
|
using osu.Framework.Audio.Sample;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Rulesets.Objects.Legacy;
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
@ -88,10 +89,12 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
|||||||
switch (maniaComponent.Component)
|
switch (maniaComponent.Component)
|
||||||
{
|
{
|
||||||
case ManiaSkinComponents.ColumnBackground:
|
case ManiaSkinComponents.ColumnBackground:
|
||||||
return new LegacyColumnBackground(maniaComponent.TargetColumn == beatmap.TotalColumns - 1);
|
return new LegacyColumnBackground();
|
||||||
|
|
||||||
case ManiaSkinComponents.HitTarget:
|
case ManiaSkinComponents.HitTarget:
|
||||||
return new LegacyHitTarget();
|
// Legacy skins sandwich the hit target between the column background and the column light.
|
||||||
|
// To preserve this ordering, it's created manually inside LegacyStageBackground.
|
||||||
|
return Drawable.Empty();
|
||||||
|
|
||||||
case ManiaSkinComponents.KeyArea:
|
case ManiaSkinComponents.KeyArea:
|
||||||
return new LegacyKeyArea();
|
return new LegacyKeyArea();
|
||||||
@ -112,7 +115,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
|||||||
return new LegacyHitExplosion();
|
return new LegacyHitExplosion();
|
||||||
|
|
||||||
case ManiaSkinComponents.StageBackground:
|
case ManiaSkinComponents.StageBackground:
|
||||||
return new LegacyStageBackground();
|
Debug.Assert(maniaComponent.StageDefinition != null);
|
||||||
|
return new LegacyStageBackground(maniaComponent.StageDefinition.Value);
|
||||||
|
|
||||||
case ManiaSkinComponents.StageForeground:
|
case ManiaSkinComponents.StageForeground:
|
||||||
return new LegacyStageForeground();
|
return new LegacyStageForeground();
|
||||||
|
@ -17,6 +17,7 @@ using osu.Game.Rulesets.UI.Scrolling;
|
|||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.UI
|
namespace osu.Game.Rulesets.Mania.UI
|
||||||
{
|
{
|
||||||
@ -36,6 +37,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
public readonly ColumnHitObjectArea HitObjectArea;
|
public readonly ColumnHitObjectArea HitObjectArea;
|
||||||
internal readonly Container TopLevelContainer;
|
internal readonly Container TopLevelContainer;
|
||||||
private readonly DrawablePool<PoolableHitExplosion> hitExplosionPool;
|
private readonly DrawablePool<PoolableHitExplosion> hitExplosionPool;
|
||||||
|
private readonly OrderedHitPolicy hitPolicy;
|
||||||
|
|
||||||
public Container UnderlayElements => HitObjectArea.UnderlayElements;
|
public Container UnderlayElements => HitObjectArea.UnderlayElements;
|
||||||
|
|
||||||
@ -65,11 +67,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
|
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
|
||||||
|
|
||||||
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
|
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Axes RelativeSizeAxes => Axes.Y;
|
|
||||||
|
|
||||||
public ColumnType ColumnType { get; set; }
|
public ColumnType ColumnType { get; set; }
|
||||||
|
|
||||||
public bool IsSpecial => ColumnType == ColumnType.Special;
|
public bool IsSpecial => ColumnType == ColumnType.Special;
|
||||||
@ -92,6 +94,9 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
hitObject.AccentColour.Value = AccentColour;
|
hitObject.AccentColour.Value = AccentColour;
|
||||||
hitObject.OnNewResult += OnNewResult;
|
hitObject.OnNewResult += OnNewResult;
|
||||||
|
|
||||||
|
DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject;
|
||||||
|
maniaObject.CheckHittable = hitPolicy.IsHittable;
|
||||||
|
|
||||||
HitObjectContainer.Add(hitObject);
|
HitObjectContainer.Add(hitObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +111,9 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
|
|
||||||
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
||||||
{
|
{
|
||||||
|
if (result.IsHit)
|
||||||
|
hitPolicy.HandleHit(judgedObject);
|
||||||
|
|
||||||
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
|
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
105
osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
Normal file
105
osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
// 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 osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Skinning;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="Drawable"/> which flows its contents according to the <see cref="Column"/>s in a <see cref="Stage"/>.
|
||||||
|
/// Content can be added to individual columns via <see cref="SetContentForColumn"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TContent">The type of content in each column.</typeparam>
|
||||||
|
public class ColumnFlow<TContent> : CompositeDrawable
|
||||||
|
where TContent : Drawable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// All contents added to this <see cref="ColumnFlow{TContent}"/>.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<TContent> Content => columns.Children.Select(c => c.Count == 0 ? null : (TContent)c.Child).ToList();
|
||||||
|
|
||||||
|
private readonly FillFlowContainer<Container> columns;
|
||||||
|
private readonly StageDefinition stageDefinition;
|
||||||
|
|
||||||
|
public ColumnFlow(StageDefinition stageDefinition)
|
||||||
|
{
|
||||||
|
this.stageDefinition = stageDefinition;
|
||||||
|
|
||||||
|
AutoSizeAxes = Axes.X;
|
||||||
|
|
||||||
|
InternalChild = columns = new FillFlowContainer<Container>
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
AutoSizeAxes = Axes.X,
|
||||||
|
Direction = FillDirection.Horizontal,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < stageDefinition.Columns; i++)
|
||||||
|
columns.Add(new Container { RelativeSizeAxes = Axes.Y });
|
||||||
|
}
|
||||||
|
|
||||||
|
private ISkinSource currentSkin;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(ISkinSource skin)
|
||||||
|
{
|
||||||
|
currentSkin = skin;
|
||||||
|
|
||||||
|
skin.SourceChanged += onSkinChanged;
|
||||||
|
onSkinChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSkinChanged()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < stageDefinition.Columns; i++)
|
||||||
|
{
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
float spacing = currentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||||
|
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1))
|
||||||
|
?.Value ?? Stage.COLUMN_SPACING;
|
||||||
|
|
||||||
|
columns[i].Margin = new MarginPadding { Left = spacing };
|
||||||
|
}
|
||||||
|
|
||||||
|
float? width = currentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||||
|
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i))
|
||||||
|
?.Value;
|
||||||
|
|
||||||
|
if (width == null)
|
||||||
|
// only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration)
|
||||||
|
columns[i].Width = stageDefinition.IsSpecialColumn(i) ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH;
|
||||||
|
else
|
||||||
|
columns[i].Width = width.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the content of one of the columns of this <see cref="ColumnFlow{TContent}"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="column">The index of the column to set the content of.</param>
|
||||||
|
/// <param name="content">The content.</param>
|
||||||
|
public void SetContentForColumn(int column, TContent content) => columns[column].Child = content;
|
||||||
|
|
||||||
|
public new MarginPadding Padding
|
||||||
|
{
|
||||||
|
get => base.Padding;
|
||||||
|
set => base.Padding = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
if (currentSkin != null)
|
||||||
|
currentSkin.SourceChanged -= onSkinChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -31,12 +31,12 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The minimum time range. This occurs at a <see cref="relativeTimeRange"/> of 40.
|
/// The minimum time range. This occurs at a <see cref="relativeTimeRange"/> of 40.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const double MIN_TIME_RANGE = 150;
|
public const double MIN_TIME_RANGE = 340;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The maximum time range. This occurs at a <see cref="relativeTimeRange"/> of 1.
|
/// The maximum time range. This occurs at a <see cref="relativeTimeRange"/> of 1.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const double MAX_TIME_RANGE = 6000;
|
public const double MAX_TIME_RANGE = 13720;
|
||||||
|
|
||||||
protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
|
protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
|
||||||
|
|
||||||
|
78
osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs
Normal file
78
osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures that only the most recent <see cref="HitObject"/> is hittable, affectionately known as "note lock".
|
||||||
|
/// </summary>
|
||||||
|
public class OrderedHitPolicy
|
||||||
|
{
|
||||||
|
private readonly HitObjectContainer hitObjectContainer;
|
||||||
|
|
||||||
|
public OrderedHitPolicy(HitObjectContainer hitObjectContainer)
|
||||||
|
{
|
||||||
|
this.hitObjectContainer = hitObjectContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether a <see cref="DrawableHitObject"/> can be hit at a point in time.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Only the most recent <see cref="DrawableHitObject"/> can be hit, a previous hitobject's window cannot extend past the next one.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to check.</param>
|
||||||
|
/// <param name="time">The time to check.</param>
|
||||||
|
/// <returns>Whether <paramref name="hitObject"/> can be hit at the given <paramref name="time"/>.</returns>
|
||||||
|
public bool IsHittable(DrawableHitObject hitObject, double time)
|
||||||
|
{
|
||||||
|
var nextObject = hitObjectContainer.AliveObjects.GetNext(hitObject);
|
||||||
|
return nextObject == null || time < nextObject.HitObject.StartTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles a <see cref="HitObject"/> being hit to potentially miss all earlier <see cref="HitObject"/>s.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hitObject">The <see cref="HitObject"/> that was hit.</param>
|
||||||
|
public void HandleHit(DrawableHitObject hitObject)
|
||||||
|
{
|
||||||
|
if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset))
|
||||||
|
throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
|
||||||
|
|
||||||
|
foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
|
||||||
|
{
|
||||||
|
if (obj.Judged)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
((DrawableManiaHitObject)obj).MissForcefully();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
|
||||||
|
{
|
||||||
|
foreach (var obj in hitObjectContainer.AliveObjects)
|
||||||
|
{
|
||||||
|
if (obj.HitObject.GetEndTime() >= targetTime)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
yield return obj;
|
||||||
|
|
||||||
|
foreach (var nestedObj in obj.NestedHitObjects)
|
||||||
|
{
|
||||||
|
if (nestedObj.HitObject.GetEndTime() >= targetTime)
|
||||||
|
break;
|
||||||
|
|
||||||
|
yield return nestedObj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Pooling;
|
using osu.Framework.Graphics.Pooling;
|
||||||
@ -11,7 +10,6 @@ using osu.Game.Rulesets.Judgements;
|
|||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Mania.Skinning;
|
|
||||||
using osu.Game.Rulesets.Mania.UI.Components;
|
using osu.Game.Rulesets.Mania.UI.Components;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
@ -31,14 +29,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
|
|
||||||
public const float HIT_TARGET_POSITION = 110;
|
public const float HIT_TARGET_POSITION = 110;
|
||||||
|
|
||||||
public IReadOnlyList<Column> Columns => columnFlow.Children;
|
public IReadOnlyList<Column> Columns => columnFlow.Content;
|
||||||
private readonly FillFlowContainer<Column> columnFlow;
|
private readonly ColumnFlow<Column> columnFlow;
|
||||||
|
|
||||||
private readonly JudgementContainer<DrawableManiaJudgement> judgements;
|
private readonly JudgementContainer<DrawableManiaJudgement> judgements;
|
||||||
private readonly DrawablePool<DrawableManiaJudgement> judgementPool;
|
private readonly DrawablePool<DrawableManiaJudgement> judgementPool;
|
||||||
|
|
||||||
private readonly Drawable barLineContainer;
|
private readonly Drawable barLineContainer;
|
||||||
private readonly Container topLevelContainer;
|
|
||||||
|
|
||||||
private readonly Dictionary<ColumnType, Color4> columnColours = new Dictionary<ColumnType, Color4>
|
private readonly Dictionary<ColumnType, Color4> columnColours = new Dictionary<ColumnType, Color4>
|
||||||
{
|
{
|
||||||
@ -62,6 +59,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
RelativeSizeAxes = Axes.Y;
|
RelativeSizeAxes = Axes.Y;
|
||||||
AutoSizeAxes = Axes.X;
|
AutoSizeAxes = Axes.X;
|
||||||
|
|
||||||
|
Container topLevelContainer;
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
judgementPool = new DrawablePool<DrawableManiaJudgement>(2),
|
judgementPool = new DrawablePool<DrawableManiaJudgement>(2),
|
||||||
@ -73,17 +72,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
AutoSizeAxes = Axes.X,
|
AutoSizeAxes = Axes.X,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground())
|
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: definition), _ => new DefaultStageBackground())
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
},
|
},
|
||||||
columnFlow = new FillFlowContainer<Column>
|
columnFlow = new ColumnFlow<Column>(definition)
|
||||||
{
|
{
|
||||||
Name = "Columns",
|
|
||||||
RelativeSizeAxes = Axes.Y,
|
RelativeSizeAxes = Axes.Y,
|
||||||
AutoSizeAxes = Axes.X,
|
|
||||||
Direction = FillDirection.Horizontal,
|
|
||||||
Padding = new MarginPadding { Left = COLUMN_SPACING, Right = COLUMN_SPACING },
|
|
||||||
},
|
},
|
||||||
new Container
|
new Container
|
||||||
{
|
{
|
||||||
@ -102,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
RelativeSizeAxes = Axes.Y,
|
RelativeSizeAxes = Axes.Y,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null)
|
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: definition), _ => null)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
},
|
},
|
||||||
@ -121,60 +116,22 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
for (int i = 0; i < definition.Columns; i++)
|
for (int i = 0; i < definition.Columns; i++)
|
||||||
{
|
{
|
||||||
var columnType = definition.GetTypeOfColumn(i);
|
var columnType = definition.GetTypeOfColumn(i);
|
||||||
|
|
||||||
var column = new Column(firstColumnIndex + i)
|
var column = new Column(firstColumnIndex + i)
|
||||||
{
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Width = 1,
|
||||||
ColumnType = columnType,
|
ColumnType = columnType,
|
||||||
AccentColour = columnColours[columnType],
|
AccentColour = columnColours[columnType],
|
||||||
Action = { Value = columnType == ColumnType.Special ? specialColumnStartAction++ : normalColumnStartAction++ }
|
Action = { Value = columnType == ColumnType.Special ? specialColumnStartAction++ : normalColumnStartAction++ }
|
||||||
};
|
};
|
||||||
|
|
||||||
AddColumn(column);
|
topLevelContainer.Add(column.TopLevelContainer.CreateProxy());
|
||||||
|
columnFlow.SetContentForColumn(i, column);
|
||||||
|
AddNested(column);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ISkin currentSkin;
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(ISkinSource skin)
|
|
||||||
{
|
|
||||||
currentSkin = skin;
|
|
||||||
skin.SourceChanged += onSkinChanged;
|
|
||||||
|
|
||||||
onSkinChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onSkinChanged()
|
|
||||||
{
|
|
||||||
foreach (var col in columnFlow)
|
|
||||||
{
|
|
||||||
if (col.Index > 0)
|
|
||||||
{
|
|
||||||
float spacing = currentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
|
||||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, col.Index - 1))
|
|
||||||
?.Value ?? COLUMN_SPACING;
|
|
||||||
|
|
||||||
col.Margin = new MarginPadding { Left = spacing };
|
|
||||||
}
|
|
||||||
|
|
||||||
float? width = currentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
|
||||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, col.Index))
|
|
||||||
?.Value;
|
|
||||||
|
|
||||||
if (width == null)
|
|
||||||
// only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration)
|
|
||||||
col.Width = col.IsSpecial ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH;
|
|
||||||
else
|
|
||||||
col.Width = width.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddColumn(Column c)
|
|
||||||
{
|
|
||||||
topLevelContainer.Add(c.TopLevelContainer.CreateProxy());
|
|
||||||
columnFlow.Add(c);
|
|
||||||
AddNested(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Add(DrawableHitObject h)
|
public override void Add(DrawableHitObject h)
|
||||||
{
|
{
|
||||||
var maniaObject = (ManiaHitObject)h.HitObject;
|
var maniaObject = (ManiaHitObject)h.HitObject;
|
||||||
|
65
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
Normal file
65
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
Normal 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||||
|
{
|
||||||
|
public class TestSceneOsuModSpunOut : OsuModTestScene
|
||||||
|
{
|
||||||
|
protected override bool AllowFail => true;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSpinnerAutoCompleted() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new OsuModSpunOut(),
|
||||||
|
Autoplay = false,
|
||||||
|
Beatmap = singleSpinnerBeatmap,
|
||||||
|
PassCondition = () => Player.ChildrenOfType<DrawableSpinner>().Single().Progress >= 1
|
||||||
|
});
|
||||||
|
|
||||||
|
[TestCase(null)]
|
||||||
|
[TestCase(typeof(OsuModDoubleTime))]
|
||||||
|
[TestCase(typeof(OsuModHalfTime))]
|
||||||
|
public void TestSpinRateUnaffectedByMods(Type additionalModType)
|
||||||
|
{
|
||||||
|
var mods = new List<Mod> { new OsuModSpunOut() };
|
||||||
|
if (additionalModType != null)
|
||||||
|
mods.Add((Mod)Activator.CreateInstance(additionalModType));
|
||||||
|
|
||||||
|
CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mods = mods,
|
||||||
|
Autoplay = false,
|
||||||
|
Beatmap = singleSpinnerBeatmap,
|
||||||
|
PassCondition = () => Precision.AlmostEquals(Player.ChildrenOfType<SpinnerSpmCounter>().Single().SpinsPerMinute, 286, 1)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Beatmap singleSpinnerBeatmap => new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new Spinner
|
||||||
|
{
|
||||||
|
Position = new Vector2(256, 192),
|
||||||
|
StartTime = 500,
|
||||||
|
Duration = 2000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
@ -22,7 +22,6 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
|
|||||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
||||||
using osu.Game.Storyboards;
|
using osu.Game.Storyboards;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
{
|
{
|
||||||
@ -32,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private AudioManager audioManager { get; set; }
|
private AudioManager audioManager { get; set; }
|
||||||
|
|
||||||
private TrackVirtualManual track;
|
|
||||||
|
|
||||||
protected override bool Autoplay => autoplay;
|
protected override bool Autoplay => autoplay;
|
||||||
private bool autoplay;
|
private bool autoplay;
|
||||||
|
|
||||||
@ -44,11 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private const double fade_in_modifier = -1200;
|
private const double fade_in_modifier = -1200;
|
||||||
|
|
||||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
||||||
{
|
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||||
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
|
||||||
track = (TrackVirtualManual)working.Track;
|
|
||||||
return working;
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(RulesetConfigCache configCache)
|
private void load(RulesetConfigCache configCache)
|
||||||
@ -72,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
AddStep("enable autoplay", () => autoplay = true);
|
AddStep("enable autoplay", () => autoplay = true);
|
||||||
base.SetUpSteps();
|
base.SetUpSteps();
|
||||||
AddUntilStep("wait for track to start running", () => track.IsRunning);
|
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
|
||||||
|
|
||||||
double startTime = hitObjects[sliderIndex].StartTime;
|
double startTime = hitObjects[sliderIndex].StartTime;
|
||||||
retrieveDrawableSlider(sliderIndex);
|
retrieveDrawableSlider(sliderIndex);
|
||||||
@ -97,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
AddStep("have autoplay", () => autoplay = true);
|
AddStep("have autoplay", () => autoplay = true);
|
||||||
base.SetUpSteps();
|
base.SetUpSteps();
|
||||||
AddUntilStep("wait for track to start running", () => track.IsRunning);
|
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
|
||||||
|
|
||||||
double startTime = hitObjects[sliderIndex].StartTime;
|
double startTime = hitObjects[sliderIndex].StartTime;
|
||||||
retrieveDrawableSlider(sliderIndex);
|
retrieveDrawableSlider(sliderIndex);
|
||||||
@ -201,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
private void addSeekStep(double time)
|
private void addSeekStep(double time)
|
||||||
{
|
{
|
||||||
AddStep($"seek to {time}", () => track.Seek(time));
|
AddStep($"seek to {time}", () => MusicController.SeekTo(time));
|
||||||
|
|
||||||
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
|
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ using osu.Game.Beatmaps.ControlPoints;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
{
|
{
|
||||||
@ -62,7 +63,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
drawableSpinner = new TestDrawableSpinner(spinner, auto)
|
drawableSpinner = new TestDrawableSpinner(spinner, auto)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Depth = depthIndex++
|
Depth = depthIndex++,
|
||||||
|
Scale = new Vector2(0.75f)
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
|
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
|
||||||
|
@ -25,7 +25,6 @@ using osu.Game.Scoring;
|
|||||||
using osu.Game.Storyboards;
|
using osu.Game.Storyboards;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
{
|
{
|
||||||
@ -34,18 +33,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private AudioManager audioManager { get; set; }
|
private AudioManager audioManager { get; set; }
|
||||||
|
|
||||||
private TrackVirtualManual track;
|
|
||||||
|
|
||||||
protected override bool Autoplay => true;
|
protected override bool Autoplay => true;
|
||||||
|
|
||||||
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
|
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
|
||||||
|
|
||||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
||||||
{
|
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||||
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
|
||||||
track = (TrackVirtualManual)working.Track;
|
|
||||||
return working;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DrawableSpinner drawableSpinner;
|
private DrawableSpinner drawableSpinner;
|
||||||
private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType<SpriteIcon>().Single();
|
private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType<SpriteIcon>().Single();
|
||||||
@ -55,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
base.SetUpSteps();
|
base.SetUpSteps();
|
||||||
|
|
||||||
AddUntilStep("wait for track to start running", () => track.IsRunning);
|
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
|
||||||
AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First());
|
AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
addSeekStep(0);
|
addSeekStep(0);
|
||||||
|
|
||||||
AddStep("adjust track rate", () => track.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate)));
|
AddStep("adjust track rate", () => MusicController.CurrentTrack.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate)));
|
||||||
// autoplay replay frames use track time;
|
// autoplay replay frames use track time;
|
||||||
// if a spin takes 1000ms in track time and we're playing with a 2x rate adjustment, the spin will take 500ms of *real* time.
|
// if a spin takes 1000ms in track time and we're playing with a 2x rate adjustment, the spin will take 500ms of *real* time.
|
||||||
// therefore we need to apply the rate adjustment to the replay itself to change from track time to real time,
|
// therefore we need to apply the rate adjustment to the replay itself to change from track time to real time,
|
||||||
@ -230,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
private void addSeekStep(double time)
|
private void addSeekStep(double time)
|
||||||
{
|
{
|
||||||
AddStep($"seek to {time}", () => track.Seek(time));
|
AddStep($"seek to {time}", () => MusicController.SeekTo(time));
|
||||||
|
|
||||||
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
|
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
|
||||||
}
|
}
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
// 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.Game.Beatmaps;
|
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
|
||||||
using osu.Game.Tests.Visual;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class TestSceneSpinnerSpunOut : OsuTestScene
|
|
||||||
{
|
|
||||||
[SetUp]
|
|
||||||
public void SetUp() => Schedule(() =>
|
|
||||||
{
|
|
||||||
SelectedMods.Value = new[] { new OsuModSpunOut() };
|
|
||||||
});
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestSpunOut()
|
|
||||||
{
|
|
||||||
DrawableSpinner spinner = null;
|
|
||||||
|
|
||||||
AddStep("create spinner", () => spinner = createSpinner());
|
|
||||||
|
|
||||||
AddUntilStep("wait for end", () => Time.Current > spinner.LifetimeEnd);
|
|
||||||
|
|
||||||
AddAssert("spinner is completed", () => spinner.Progress >= 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DrawableSpinner createSpinner()
|
|
||||||
{
|
|
||||||
var spinner = new Spinner
|
|
||||||
{
|
|
||||||
StartTime = Time.Current + 500,
|
|
||||||
EndTime = Time.Current + 2500
|
|
||||||
};
|
|
||||||
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
|
||||||
|
|
||||||
var drawableSpinner = new DrawableSpinner(spinner)
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
|
|
||||||
mod.ApplyToDrawableHitObjects(new[] { drawableSpinner });
|
|
||||||
|
|
||||||
Add(drawableSpinner);
|
|
||||||
return drawableSpinner;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.7.0" />
|
<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" />
|
||||||
|
@ -41,7 +41,16 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
var spinner = (DrawableSpinner)drawable;
|
var spinner = (DrawableSpinner)drawable;
|
||||||
|
|
||||||
spinner.RotationTracker.Tracking = true;
|
spinner.RotationTracker.Tracking = true;
|
||||||
spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f));
|
|
||||||
|
// early-return if we were paused to avoid division-by-zero in the subsequent calculations.
|
||||||
|
if (Precision.AlmostEquals(spinner.Clock.Rate, 0))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// because the spinner is under the gameplay clock, it is affected by rate adjustments on the track;
|
||||||
|
// for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time.
|
||||||
|
// for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here.
|
||||||
|
var rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate;
|
||||||
|
spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * 0.03f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
protected override float SamplePlaybackPosition => HitObject.X / OsuPlayfield.BASE_SIZE.X;
|
protected override float SamplePlaybackPosition => HitObject.X / OsuPlayfield.BASE_SIZE.X;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this <see cref="DrawableOsuHitObject"/> can be hit.
|
/// Whether this <see cref="DrawableOsuHitObject"/> can be hit, given a time value.
|
||||||
/// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false.
|
/// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<DrawableHitObject, double, bool> CheckHittable;
|
public Func<DrawableHitObject, double, bool> CheckHittable;
|
||||||
|
@ -12,6 +12,7 @@ using osu.Game.Graphics;
|
|||||||
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.Osu.Objects.Drawables.Pieces;
|
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
||||||
|
using osu.Game.Rulesets.Osu.Skinning;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
@ -31,6 +32,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
|
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
|
||||||
|
|
||||||
|
private bool spinnerFrequencyModulate;
|
||||||
|
|
||||||
public DrawableSpinner(Spinner s)
|
public DrawableSpinner(Spinner s)
|
||||||
: base(s)
|
: base(s)
|
||||||
{
|
{
|
||||||
@ -82,8 +85,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
}
|
}
|
||||||
|
|
||||||
private SkinnableSound spinningSample;
|
private SkinnableSound spinningSample;
|
||||||
|
private const float spinning_sample_initial_frequency = 1.0f;
|
||||||
private const float minimum_volume = 0.0001f;
|
private const float spinning_sample_modulated_base_frequency = 0.5f;
|
||||||
|
|
||||||
protected override void LoadSamples()
|
protected override void LoadSamples()
|
||||||
{
|
{
|
||||||
@ -101,8 +104,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
AddInternal(spinningSample = new SkinnableSound(clone)
|
AddInternal(spinningSample = new SkinnableSound(clone)
|
||||||
{
|
{
|
||||||
Volume = { Value = minimum_volume },
|
Volume = { Value = 0 },
|
||||||
Looping = true,
|
Looping = true,
|
||||||
|
Frequency = { Value = spinning_sample_initial_frequency }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,7 +123,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
spinningSample?.VolumeTo(minimum_volume, 200).Finally(_ => spinningSample.Stop());
|
spinningSample?.VolumeTo(0, 200).Finally(_ => spinningSample.Stop());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,6 +177,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
positionBindable.BindTo(HitObject.PositionBindable);
|
positionBindable.BindTo(HitObject.PositionBindable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void ApplySkin(ISkinSource skin, bool allowFallback)
|
||||||
|
{
|
||||||
|
base.ApplySkin(skin, allowFallback);
|
||||||
|
spinnerFrequencyModulate = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.SpinnerFrequencyModulate)?.Value ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The completion progress of this spinner from 0..1 (clamped).
|
/// The completion progress of this spinner from 0..1 (clamped).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -222,9 +232,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
if (HandleUserInput)
|
if (HandleUserInput)
|
||||||
RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
|
RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
|
||||||
|
|
||||||
if (spinningSample != null)
|
if (spinningSample != null && spinnerFrequencyModulate)
|
||||||
// todo: implement SpinnerFrequencyModulate
|
spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress;
|
||||||
spinningSample.Frequency.Value = 0.5f + Progress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateAfterChildren()
|
protected override void UpdateAfterChildren()
|
||||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
|
|
||||||
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
|
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
|
||||||
{
|
{
|
||||||
protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
|
protected override int NumericResultFor(HitResult result) => result == MaxResult ? SCORE_PER_TICK : 0;
|
||||||
|
|
||||||
protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2;
|
protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2;
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
public override bool AffectsCombo => false;
|
public override bool AffectsCombo => false;
|
||||||
|
|
||||||
protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
|
protected override int NumericResultFor(HitResult result) => result == MaxResult ? SCORE_PER_TICK : 0;
|
||||||
|
|
||||||
protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0;
|
protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0;
|
||||||
}
|
}
|
||||||
|
@ -193,13 +193,18 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
|
|
||||||
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
|
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
|
||||||
|
|
||||||
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
|
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
|
||||||
|
{
|
||||||
|
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
|
||||||
|
|
||||||
|
return new[]
|
||||||
{
|
{
|
||||||
new StatisticRow
|
new StatisticRow
|
||||||
{
|
{
|
||||||
Columns = new[]
|
Columns = new[]
|
||||||
{
|
{
|
||||||
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList())
|
new StatisticItem("Timing Distribution",
|
||||||
|
new HitEventTimingDistributionGraph(timedHitEvents)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = 250
|
Height = 250
|
||||||
@ -216,7 +221,18 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
Height = 250
|
Height = 250
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
new StatisticRow
|
||||||
|
{
|
||||||
|
Columns = new[]
|
||||||
|
{
|
||||||
|
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
||||||
|
{
|
||||||
|
new UnstableRate(timedHitEvents)
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -154,8 +154,12 @@ namespace osu.Game.Rulesets.Osu.Replays
|
|||||||
// The startPosition for the slider should not be its .Position, but the point on the circle whose tangent crosses the current cursor position
|
// The startPosition for the slider should not be its .Position, but the point on the circle whose tangent crosses the current cursor position
|
||||||
// We also modify spinnerDirection so it spins in the direction it enters the spin circle, to make a smooth transition.
|
// We also modify spinnerDirection so it spins in the direction it enters the spin circle, to make a smooth transition.
|
||||||
// TODO: Shouldn't the spinner always spin in the same direction?
|
// TODO: Shouldn't the spinner always spin in the same direction?
|
||||||
if (h is Spinner)
|
if (h is Spinner spinner)
|
||||||
{
|
{
|
||||||
|
// spinners with 0 spins required will auto-complete - don't bother
|
||||||
|
if (spinner.SpinsRequired == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[^1]).Position, out startPosition, out spinnerDirection);
|
calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[^1]).Position, out startPosition, out spinnerDirection);
|
||||||
|
|
||||||
Vector2 spinCentreOffset = SPINNER_CENTRE - ((OsuReplayFrame)Frames[^1]).Position;
|
Vector2 spinCentreOffset = SPINNER_CENTRE - ((OsuReplayFrame)Frames[^1]).Position;
|
||||||
|
@ -59,7 +59,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
hitCircleSprite = new Sprite
|
hitCircleSprite = new Sprite
|
||||||
{
|
{
|
||||||
Texture = getTextureWithFallback(string.Empty),
|
Texture = getTextureWithFallback(string.Empty),
|
||||||
Colour = drawableObject.AccentColour.Value,
|
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
},
|
},
|
||||||
@ -107,7 +106,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
state.BindValueChanged(updateState, true);
|
state.BindValueChanged(updateState, true);
|
||||||
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
|
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
|
||||||
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
|
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,8 +79,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
{
|
{
|
||||||
var spinner = (Spinner)drawableSpinner.HitObject;
|
var spinner = (Spinner)drawableSpinner.HitObject;
|
||||||
|
|
||||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
|
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
|
||||||
this.FadeInFromZero(spinner.TimePreempt / 2);
|
this.FadeInFromZero(spinner.TimeFadeIn / 2);
|
||||||
|
|
||||||
fixedMiddle.FadeColour(Color4.White);
|
fixedMiddle.FadeColour(Color4.White);
|
||||||
using (BeginAbsoluteSequence(spinner.StartTime, true))
|
using (BeginAbsoluteSequence(spinner.StartTime, true))
|
||||||
|
@ -22,11 +22,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
{
|
{
|
||||||
private DrawableSpinner drawableSpinner;
|
private DrawableSpinner drawableSpinner;
|
||||||
private Sprite disc;
|
private Sprite disc;
|
||||||
|
private Sprite metreSprite;
|
||||||
private Container metre;
|
private Container metre;
|
||||||
|
|
||||||
private const float background_y_offset = 20;
|
|
||||||
|
|
||||||
private const float sprite_scale = 1 / 1.6f;
|
private const float sprite_scale = 1 / 1.6f;
|
||||||
|
private const float final_metre_height = 692 * sprite_scale;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(ISkinSource source, DrawableHitObject drawableObject)
|
private void load(ISkinSource source, DrawableHitObject drawableObject)
|
||||||
@ -35,14 +35,22 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChild = new Container
|
||||||
|
{
|
||||||
|
// the old-style spinner relied heavily on absolute screen-space coordinate values.
|
||||||
|
// wrap everything in a container simulating absolute coords to preserve alignment
|
||||||
|
// as there are skins that depend on it.
|
||||||
|
Width = 640,
|
||||||
|
Height = 480,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new Sprite
|
new Sprite
|
||||||
{
|
{
|
||||||
Anchor = Anchor.BottomCentre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.BottomCentre,
|
Origin = Anchor.Centre,
|
||||||
Texture = source.GetTexture("spinner-background"),
|
Texture = source.GetTexture("spinner-background"),
|
||||||
Y = background_y_offset,
|
|
||||||
Scale = new Vector2(sprite_scale)
|
Scale = new Vector2(sprite_scale)
|
||||||
},
|
},
|
||||||
disc = new Sprite
|
disc = new Sprite
|
||||||
@ -54,46 +62,55 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
},
|
},
|
||||||
metre = new Container
|
metre = new Container
|
||||||
{
|
{
|
||||||
Anchor = Anchor.BottomCentre,
|
AutoSizeAxes = Axes.Both,
|
||||||
Origin = Anchor.BottomCentre,
|
// this anchor makes no sense, but that's what stable uses.
|
||||||
Y = background_y_offset,
|
Anchor = Anchor.TopLeft,
|
||||||
|
Origin = Anchor.TopLeft,
|
||||||
|
// adjustment for stable (metre has additional offset)
|
||||||
|
Margin = new MarginPadding { Top = 20 },
|
||||||
Masking = true,
|
Masking = true,
|
||||||
Child = new Sprite
|
Child = metreSprite = new Sprite
|
||||||
{
|
{
|
||||||
Texture = source.GetTexture("spinner-metre"),
|
Texture = source.GetTexture("spinner-metre"),
|
||||||
Anchor = Anchor.BottomCentre,
|
Anchor = Anchor.TopLeft,
|
||||||
Origin = Anchor.BottomCentre,
|
Origin = Anchor.TopLeft,
|
||||||
},
|
|
||||||
Scale = new Vector2(0.625f)
|
Scale = new Vector2(0.625f)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector2 metreFinalSize;
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
this.FadeOut();
|
this.FadeOut();
|
||||||
drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
|
drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
|
||||||
|
|
||||||
metreFinalSize = metre.Size = metre.Child.Size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateStateTransforms(ValueChangedEvent<ArmedState> state)
|
private void updateStateTransforms(ValueChangedEvent<ArmedState> state)
|
||||||
{
|
{
|
||||||
var spinner = drawableSpinner.HitObject;
|
var spinner = drawableSpinner.HitObject;
|
||||||
|
|
||||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
|
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
|
||||||
this.FadeInFromZero(spinner.TimePreempt / 2);
|
this.FadeInFromZero(spinner.TimeFadeIn / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
disc.Rotation = drawableSpinner.RotationTracker.Rotation;
|
disc.Rotation = drawableSpinner.RotationTracker.Rotation;
|
||||||
metre.Height = getMetreHeight(drawableSpinner.Progress);
|
|
||||||
|
// careful: need to call this exactly once for all calculations in a frame
|
||||||
|
// as the function has a random factor in it
|
||||||
|
var metreHeight = getMetreHeight(drawableSpinner.Progress);
|
||||||
|
|
||||||
|
// hack to make the metre blink up from below than down from above.
|
||||||
|
// move down the container to be able to apply masking for the metre,
|
||||||
|
// and then move the sprite back up the same amount to keep its position absolute.
|
||||||
|
metre.Y = final_metre_height - metreHeight;
|
||||||
|
metreSprite.Y = -metre.Y;
|
||||||
}
|
}
|
||||||
|
|
||||||
private const int total_bars = 10;
|
private const int total_bars = 10;
|
||||||
@ -108,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
if (RNG.NextBool(((int)progress % 10) / 10f))
|
if (RNG.NextBool(((int)progress % 10) / 10f))
|
||||||
barCount++;
|
barCount++;
|
||||||
|
|
||||||
return (float)barCount / total_bars * metreFinalSize.Y;
|
return (float)barCount / total_bars * final_metre_height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(ISkinSource skin, DrawableHitObject drawableObject)
|
private void load(ISkinSource skin, DrawableHitObject drawableObject)
|
||||||
{
|
{
|
||||||
animationContent.Colour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBall)?.Value ?? Color4.White;
|
var ballColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBall)?.Value ?? Color4.White;
|
||||||
|
|
||||||
InternalChildren = new[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
@ -39,11 +39,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
Texture = skin.GetTexture("sliderb-nd"),
|
Texture = skin.GetTexture("sliderb-nd"),
|
||||||
Colour = new Color4(5, 5, 5, 255),
|
Colour = new Color4(5, 5, 5, 255),
|
||||||
},
|
},
|
||||||
animationContent.With(d =>
|
LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d =>
|
||||||
{
|
{
|
||||||
d.Anchor = Anchor.Centre;
|
d.Anchor = Anchor.Centre;
|
||||||
d.Origin = Anchor.Centre;
|
d.Origin = Anchor.Centre;
|
||||||
}),
|
}), ballColour),
|
||||||
layerSpec = new Sprite
|
layerSpec = new Sprite
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
|
@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
CursorExpand,
|
CursorExpand,
|
||||||
CursorRotate,
|
CursorRotate,
|
||||||
HitCircleOverlayAboveNumber,
|
HitCircleOverlayAboveNumber,
|
||||||
HitCircleOverlayAboveNumer // Some old skins will have this typo
|
HitCircleOverlayAboveNumer, // Some old skins will have this typo
|
||||||
|
SpinnerFrequencyModulate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.7.0" />
|
<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" />
|
||||||
|
@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
|||||||
|
|
||||||
private void updateAccentColour()
|
private void updateAccentColour()
|
||||||
{
|
{
|
||||||
backgroundLayer.Colour = accentColour;
|
backgroundLayer.Colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,9 +76,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
|||||||
|
|
||||||
private void updateAccentColour()
|
private void updateAccentColour()
|
||||||
{
|
{
|
||||||
headCircle.AccentColour = accentColour;
|
var colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour);
|
||||||
body.Colour = accentColour;
|
|
||||||
end.Colour = accentColour;
|
headCircle.AccentColour = colour;
|
||||||
|
body.Colour = colour;
|
||||||
|
end.Colour = colour;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Game.Skinning;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Skinning
|
namespace osu.Game.Rulesets.Taiko.Skinning
|
||||||
@ -18,9 +19,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
AccentColour = component == TaikoSkinComponents.CentreHit
|
AccentColour = LegacyColourCompatibility.DisallowZeroAlpha(
|
||||||
|
component == TaikoSkinComponents.CentreHit
|
||||||
? new Color4(235, 69, 44, 255)
|
? new Color4(235, 69, 44, 255)
|
||||||
: new Color4(67, 142, 172, 255);
|
: new Color4(67, 142, 172, 255));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,19 +161,34 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
|
|
||||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
|
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
|
||||||
|
|
||||||
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
|
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
|
||||||
|
{
|
||||||
|
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList();
|
||||||
|
|
||||||
|
return new[]
|
||||||
{
|
{
|
||||||
new StatisticRow
|
new StatisticRow
|
||||||
{
|
{
|
||||||
Columns = new[]
|
Columns = new[]
|
||||||
{
|
{
|
||||||
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList())
|
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(timedHitEvents)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = 250
|
Height = 250
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
new StatisticRow
|
||||||
|
{
|
||||||
|
Columns = new[]
|
||||||
|
{
|
||||||
|
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
||||||
|
{
|
||||||
|
new UnstableRate(timedHitEvents)
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
32
osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs
Normal file
32
osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// 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.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Beatmaps
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class BeatmapDifficultyManagerTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestKeyEqualsWithDifferentModInstances()
|
||||||
|
{
|
||||||
|
var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
|
||||||
|
var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
|
||||||
|
|
||||||
|
Assert.That(key1, Is.EqualTo(key2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestKeyEqualsWithDifferentModOrder()
|
||||||
|
{
|
||||||
|
var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
|
||||||
|
var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHidden(), new OsuModHardRock() });
|
||||||
|
|
||||||
|
Assert.That(key1, Is.EqualTo(key2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -106,7 +106,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
|
|
||||||
protected override Texture GetBackground() => throw new NotImplementedException();
|
protected override Texture GetBackground() => throw new NotImplementedException();
|
||||||
|
|
||||||
protected override Track GetTrack() => throw new NotImplementedException();
|
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
70
osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
Normal file
70
osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// 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.Utils;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Catch;
|
||||||
|
using osu.Game.Rulesets.Mania;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.Taiko;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Scoring.Legacy;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Beatmaps.Formats
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class LegacyScoreDecoderTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestDecodeManiaReplay()
|
||||||
|
{
|
||||||
|
var decoder = new TestLegacyScoreDecoder();
|
||||||
|
|
||||||
|
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
|
||||||
|
{
|
||||||
|
var score = decoder.Parse(resourceStream);
|
||||||
|
|
||||||
|
Assert.AreEqual(3, score.ScoreInfo.Ruleset.ID);
|
||||||
|
|
||||||
|
Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.Great]);
|
||||||
|
Assert.AreEqual(1, score.ScoreInfo.Statistics[HitResult.Good]);
|
||||||
|
|
||||||
|
Assert.AreEqual(829_931, score.ScoreInfo.TotalScore);
|
||||||
|
Assert.AreEqual(3, score.ScoreInfo.MaxCombo);
|
||||||
|
Assert.IsTrue(Precision.AlmostEquals(0.8889, score.ScoreInfo.Accuracy, 0.0001));
|
||||||
|
Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank);
|
||||||
|
|
||||||
|
Assert.That(score.Replay.Frames, Is.Not.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestLegacyScoreDecoder : LegacyScoreDecoder
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<int, Ruleset> rulesets = new Ruleset[]
|
||||||
|
{
|
||||||
|
new OsuRuleset(),
|
||||||
|
new TaikoRuleset(),
|
||||||
|
new CatchRuleset(),
|
||||||
|
new ManiaRuleset()
|
||||||
|
}.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID);
|
||||||
|
|
||||||
|
protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId];
|
||||||
|
|
||||||
|
protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap
|
||||||
|
{
|
||||||
|
BeatmapInfo = new BeatmapInfo
|
||||||
|
{
|
||||||
|
MD5Hash = md5Hash,
|
||||||
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
|
BaseDifficulty = new BeatmapDifficulty()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -169,17 +169,17 @@ namespace osu.Game.Tests.Editing
|
|||||||
[Test]
|
[Test]
|
||||||
public void GetSnappedDistanceFromDistance()
|
public void GetSnappedDistanceFromDistance()
|
||||||
{
|
{
|
||||||
assertSnappedDistance(50, 100);
|
assertSnappedDistance(50, 0);
|
||||||
assertSnappedDistance(100, 100);
|
assertSnappedDistance(100, 100);
|
||||||
assertSnappedDistance(150, 200);
|
assertSnappedDistance(150, 100);
|
||||||
assertSnappedDistance(200, 200);
|
assertSnappedDistance(200, 200);
|
||||||
assertSnappedDistance(250, 300);
|
assertSnappedDistance(250, 200);
|
||||||
|
|
||||||
AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2);
|
AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2);
|
||||||
|
|
||||||
assertSnappedDistance(50, 0);
|
assertSnappedDistance(50, 0);
|
||||||
assertSnappedDistance(100, 200);
|
assertSnappedDistance(100, 0);
|
||||||
assertSnappedDistance(150, 200);
|
assertSnappedDistance(150, 0);
|
||||||
assertSnappedDistance(200, 200);
|
assertSnappedDistance(200, 200);
|
||||||
assertSnappedDistance(250, 200);
|
assertSnappedDistance(250, 200);
|
||||||
|
|
||||||
@ -190,8 +190,8 @@ namespace osu.Game.Tests.Editing
|
|||||||
});
|
});
|
||||||
|
|
||||||
assertSnappedDistance(50, 0);
|
assertSnappedDistance(50, 0);
|
||||||
assertSnappedDistance(100, 200);
|
assertSnappedDistance(100, 0);
|
||||||
assertSnappedDistance(150, 200);
|
assertSnappedDistance(150, 0);
|
||||||
assertSnappedDistance(200, 200);
|
assertSnappedDistance(200, 200);
|
||||||
assertSnappedDistance(250, 200);
|
assertSnappedDistance(250, 200);
|
||||||
assertSnappedDistance(400, 400);
|
assertSnappedDistance(400, 400);
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
// 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;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
@ -19,7 +17,14 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
{
|
{
|
||||||
GameplayClockContainer gcc = null;
|
GameplayClockContainer gcc = null;
|
||||||
|
|
||||||
AddStep("create container", () => Add(gcc = new GameplayClockContainer(CreateWorkingBeatmap(new OsuRuleset().RulesetInfo), Array.Empty<Mod>(), 0)));
|
AddStep("create container", () =>
|
||||||
|
{
|
||||||
|
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||||
|
working.LoadTrack();
|
||||||
|
|
||||||
|
Add(gcc = new GameplayClockContainer(working, 0));
|
||||||
|
});
|
||||||
|
|
||||||
AddStep("start track", () => gcc.Start());
|
AddStep("start track", () => gcc.Start());
|
||||||
AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0);
|
AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0);
|
||||||
}
|
}
|
||||||
|
41
osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
Normal file
41
osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// 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.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Gameplay
|
||||||
|
{
|
||||||
|
[HeadlessTest]
|
||||||
|
public class TestSceneScoreProcessor : OsuTestScene
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestNoScoreIncreaseFromMiss()
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap<TestHitObject> { HitObjects = { new TestHitObject() } };
|
||||||
|
|
||||||
|
var scoreProcessor = new ScoreProcessor();
|
||||||
|
scoreProcessor.ApplyBeatmap(beatmap);
|
||||||
|
|
||||||
|
// Apply a miss judgement
|
||||||
|
scoreProcessor.ApplyResult(new JudgementResult(new TestHitObject(), new TestJudgement()) { Type = HitResult.Miss });
|
||||||
|
|
||||||
|
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestHitObject : HitObject
|
||||||
|
{
|
||||||
|
public override Judgement CreateJudgement() => new TestJudgement();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestJudgement : Judgement
|
||||||
|
{
|
||||||
|
protected override int NumericResultFor(HitResult result) => 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user