1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 15:27:24 +08:00

Merge branch 'master' into morth-taiko-changes

This commit is contained in:
smoogipoo 2020-09-03 17:35:26 +09:00
commit d387c0cba0
242 changed files with 3983 additions and 1320 deletions

View File

@ -191,4 +191,7 @@ dotnet_diagnostic.IDE0052.severity = silent
#Rules for disposable #Rules for disposable
dotnet_diagnostic.IDE0067.severity = none dotnet_diagnostic.IDE0067.severity = none
dotnet_diagnostic.IDE0068.severity = none dotnet_diagnostic.IDE0068.severity = none
dotnet_diagnostic.IDE0069.severity = none dotnet_diagnostic.IDE0069.severity = none
#Disable operator overloads requiring alternate named methods
dotnet_diagnostic.CA2225.severity = none

View File

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

View File

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

View File

@ -9,7 +9,9 @@
[![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu) [![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu)
[![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy) [![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew. A free-to-win rhythm game. Rhythm is just a *click* away!
The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew.
## Status ## Status

View File

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

View File

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

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.731.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.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>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,84 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests.Mods
{
public class TestSceneCatchModRelax : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
[Test]
public void TestModRelax() => CreateModTest(new ModTestData
{
Mod = new CatchModRelax(),
Autoplay = false,
PassCondition = passCondition,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Fruit
{
X = CatchPlayfield.CENTER_X,
StartTime = 0
},
new Fruit
{
X = 0,
StartTime = 250
},
new Fruit
{
X = CatchPlayfield.WIDTH,
StartTime = 500
},
new JuiceStream
{
X = CatchPlayfield.CENTER_X,
StartTime = 750,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 })
}
}
}
});
private bool passCondition()
{
var playfield = this.ChildrenOfType<CatchPlayfield>().Single();
switch (Player.ScoreProcessor.Combo.Value)
{
case 0:
InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre);
break;
case 1:
InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.BottomLeft);
break;
case 2:
InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.BottomRight);
break;
case 3:
InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre);
break;
}
return Player.ScoreProcessor.Combo.Value >= 6;
}
}
}

View File

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

View File

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

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -18,23 +19,43 @@ 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;
// 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);
} }
} }
private Catcher getCatcher() => Player.ChildrenOfType<CatcherArea>().First().MovableCatcher;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{ {
var beatmap = new Beatmap var beatmap = new Beatmap
@ -46,6 +67,8 @@ namespace osu.Game.Rulesets.Catch.Tests
} }
}; };
beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
// Should produce a hyper-dash (edge case test) // Should produce a hyper-dash (edge case test)
beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56, NewCombo = true }); beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56, NewCombo = true });
beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308, NewCombo = true }); beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308, NewCombo = true });
@ -63,6 +86,20 @@ namespace osu.Game.Rulesets.Catch.Tests
createObjects(() => new Fruit { X = right_x }); createObjects(() => new Fruit { X = right_x });
createObjects(() => new TestJuiceStream(left_x), 1); createObjects(() => new TestJuiceStream(left_x), 1);
beatmap.ControlPointInfo.Add(startTime, new TimingControlPoint
{
BeatLength = 50
});
createObjects(() => new TestJuiceStream(left_x)
{
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero),
new PathControlPoint(new Vector2(512, 0))
})
}, 1);
return beatmap; return beatmap;
void createObjects(Func<CatchHitObject> createObject, int count = 3) void createObjects(Func<CatchHitObject> createObject, int count = 3)

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.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" />

View File

@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
if (amount > 0) if (amount > 0)
{ {
// Clamp to the right bound // Clamp to the right bound
if (position + amount < 1) if (position + amount < CatchPlayfield.WIDTH)
position += amount; position += amount;
} }
else else
@ -212,6 +212,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2; double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2;
// Todo: This is wrong. osu!stable calculated hyperdashes using the full catcher size, excluding the margins.
// This should theoretically cause impossible scenarios, but practically, likely due to the size of the playfield, it doesn't seem possible.
// For now, to bring gameplay (and diffcalc!) completely in-line with stable, this code also uses the full catcher size.
halfCatcherWidth /= Catcher.ALLOWED_CATCH_RANGE;
int lastDirection = 0; int lastDirection = 0;
double lastExcess = halfCatcherWidth; double lastExcess = halfCatcherWidth;

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Mods
protected override bool OnMouseMove(MouseMoveEvent e) protected override bool OnMouseMove(MouseMoveEvent e)
{ {
catcher.UpdatePosition(e.MousePosition.X / DrawSize.X); catcher.UpdatePosition(e.MousePosition.X / DrawSize.X * CatchPlayfield.WIDTH);
return base.OnMouseMove(e); return base.OnMouseMove(e);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,69 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public class DropletPiece : CompositeDrawable
{
public DropletPiece()
{
Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2);
}
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject)
{
DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject;
var hitObject = drawableCatchObject.HitObject;
InternalChild = new Pulp
{
RelativeSizeAxes = Axes.Both,
AccentColour = { BindTarget = drawableObject.AccentColour }
};
if (hitObject.HyperDash)
{
AddInternal(new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(2f),
Depth = 1,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
BorderThickness = 6,
Children = new Drawable[]
{
new Box
{
AlwaysPresent = true,
Alpha = 0.3f,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
}
}
}
}
});
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,20 @@
osu file format v14
[General]
StackLeniency: 0.7
Mode: 2
[Difficulty]
HPDrainRate:6
CircleSize:4
OverallDifficulty:9.6
ApproachRate:9.6
SliderMultiplier:1.9
SliderTickRate:1
[TimingPoints]
2169,266.666666666667,4,2,1,70,1,0
[HitObjects]
374,60,3368,1,0,0:0:0:0:
410,146,3501,1,2,0:1:0:0:

View File

@ -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);

View File

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

View File

@ -10,15 +10,21 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
public class CatchPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer public class CatchPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{ {
private const float playfield_size_adjust = 0.8f;
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => content;
private readonly Container content; private readonly Container content;
public CatchPlayfieldAdjustmentContainer() public CatchPlayfieldAdjustmentContainer()
{ {
Anchor = Anchor.TopCentre; // because we are using centre anchor/origin, we will need to limit visibility in the future
Origin = Anchor.TopCentre; // to ensure tall windows do not get a readability advantage.
// it may be possible to bake the catch-specific offsets (-100..340 mentioned below) into new values
// which are compatible with TopCentre alignment.
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Size = new Vector2(0.86f); // matches stable's vertical offset for catcher plate Size = new Vector2(playfield_size_adjust);
InternalChild = new Container InternalChild = new Container
{ {
@ -27,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.UI
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit, FillMode = FillMode.Fit,
FillAspectRatio = 4f / 3, FillAspectRatio = 4f / 3,
Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both } Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both, }
}; };
} }
@ -40,8 +46,14 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
base.Update(); base.Update();
// in stable, fruit fall vertically from -100 to 340.
// to emulate this, we want to make our playfield 440 gameplay pixels high.
// we then offset it -100 vertically in the position set below.
const float stable_v_offset_ratio = 440 / 384f;
Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.WIDTH); Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.WIDTH);
Size = Vector2.Divide(Vector2.One, Scale); Position = new Vector2(0, -100 * stable_v_offset_ratio + Scale.X);
Size = Vector2.Divide(new Vector2(1, stable_v_offset_ratio), Scale);
} }
} }
} }

View File

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

View File

@ -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);

View File

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

View File

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

View File

@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public class TestSceneManiaModInvert : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[Test]
public void TestInversion() => CreateModTest(new ModTestData
{
Mod = new ManiaModInvert(),
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2
});
}
}

View File

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

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Children = new Drawable[] Children = new Drawable[]
{ {
new ColumnTestContainer(0, ManiaAction.Key1) new ColumnTestContainer(0, ManiaAction.Key1, true)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
})); }));
}) })
}, },
new ColumnTestContainer(1, ManiaAction.Key2) new ColumnTestContainer(1, ManiaAction.Key2, true)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -3,6 +3,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -13,7 +14,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground()) SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: new StageDefinition { Columns = 4 }),
_ => new DefaultStageBackground())
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -3,6 +3,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Tests.Skinning namespace osu.Game.Rulesets.Mania.Tests.Skinning
@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null) SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: new StageDefinition { Columns = 4 }), _ => null)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -0,0 +1,117 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
public class TestSceneOutOfOrderHits : RateAdjustedBeatmapTestScene
{
[Test]
public void TestPreviousHitWindowDoesNotExtendPastNextObject()
{
var objects = new List<ManiaHitObject>();
var frames = new List<ReplayFrame>();
for (int i = 0; i < 7; i++)
{
double time = 1000 + i * 100;
objects.Add(new Note { StartTime = time });
if (i > 0)
{
frames.Add(new ManiaReplayFrame(time + 10, ManiaAction.Key1));
frames.Add(new ManiaReplayFrame(time + 11));
}
}
performTest(objects, frames);
addJudgementAssert(objects[0], HitResult.Miss);
for (int i = 1; i < 7; i++)
{
addJudgementAssert(objects[i], HitResult.Perfect);
addJudgementOffsetAssert(objects[i], 10);
}
}
private void addJudgementAssert(ManiaHitObject hitObject, HitResult result)
{
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
() => judgementResults.Single(r => r.HitObject == hitObject).Type == result);
}
private void addJudgementOffsetAssert(ManiaHitObject hitObject, double offset)
{
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
() => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
}
private ScoreAccessibleReplayPlayer currentPlayer;
private List<JudgementResult> judgementResults;
private void performTest(List<ManiaHitObject> hitObjects, List<ReplayFrame> frames)
{
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })
{
HitObjects = hitObjects,
BeatmapInfo =
{
Ruleset = new ManiaRuleset().RulesetInfo
},
});
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
{
p.ScoreProcessor.NewJudgement += result =>
{
if (currentPlayer == p) judgementResults.Add(result);
};
};
LoadScreen(currentPlayer = p);
judgementResults = new List<JudgementResult>();
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
}
private class ScoreAccessibleReplayPlayer : ReplayPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
: base(score, false, false)
{
}
}
}
}

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.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" />

View File

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

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using 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);
} }
} }

View File

@ -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)
{ {

View File

@ -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)
}))
}
} }
}; };
} }

View File

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

View 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();
}
}
}

View File

@ -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()

View File

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

View File

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

View File

@ -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);
} }

View File

@ -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,60 +117,51 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
} }
} }
} }
}
};
if (drawableObject != null)
{
var holdNote = (DrawableHoldNote)drawableObject;
AccentColour.BindTo(drawableObject.AccentColour);
isHitting.BindTo(holdNote.IsHitting);
}
AccentColour.BindValueChanged(onAccentChanged, true);
isHitting.BindValueChanged(_ => onAccentChanged(new ValueChangedEvent<Color4>(AccentColour.Value, AccentColour.Value)), true);
}
private void onAccentChanged(ValueChangedEvent<Color4> accent)
{
Foreground.Colour = accent.NewValue.Opacity(0.5f);
Background.Colour = accent.NewValue.Opacity(0.7f);
const float animation_length = 50;
Foreground.ClearTransforms(false, nameof(Foreground.Colour));
if (isHitting.Value)
{
// wait for the next sync point
double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
using (Foreground.BeginDelayedSequence(synchronisedOffset))
Foreground.FadeColour(accent.NewValue.Lighten(0.2f), animation_length).Then().FadeColour(Foreground.Colour, animation_length).Loop();
}
subtractionCache.Invalidate();
}
protected override void Update()
{
base.Update();
if (!subtractionCache.IsValid)
{
subtractionLayer.Width = 5;
subtractionLayer.Height = Math.Max(0, DrawHeight - DrawWidth);
subtractionLayer.EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.White,
Type = EdgeEffectType.Glow,
Radius = DrawWidth
}; };
Foreground.ForceRedraw(); AccentColour.BindValueChanged(onAccentChanged, true);
subtractionContainer.ForceRedraw(); IsHitting.BindValueChanged(_ => onAccentChanged(new ValueChangedEvent<Color4>(AccentColour.Value, AccentColour.Value)), true);
}
subtractionCache.Validate(); private void onAccentChanged(ValueChangedEvent<Color4> accent)
{
foregroundBuffer.Colour = accent.NewValue.Opacity(0.5f);
const float animation_length = 50;
foregroundBuffer.ClearTransforms(false, nameof(foregroundBuffer.Colour));
if (IsHitting.Value)
{
// wait for the next sync point
double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
using (foregroundBuffer.BeginDelayedSequence(synchronisedOffset))
foregroundBuffer.FadeColour(accent.NewValue.Lighten(0.2f), animation_length).Then().FadeColour(foregroundBuffer.Colour, animation_length).Loop();
}
subtractionCache.Invalidate();
}
protected override void Update()
{
base.Update();
if (!subtractionCache.IsValid)
{
subtractionLayer.Width = 5;
subtractionLayer.Height = Math.Max(0, DrawHeight - DrawWidth);
subtractionLayer.EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.White,
Type = EdgeEffectType.Glow,
Radius = DrawWidth
};
foregroundBuffer.ForceRedraw();
subtractionBuffer.ForceRedraw();
subtractionCache.Validate();
}
} }
} }
} }

View File

@ -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();
}
}

View File

@ -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;
} }
} }

View 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 };
}
}
}

View File

@ -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();
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
},
}
}
};
}
}
} }
} }

View File

@ -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();

View File

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

View 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;
}
}
}

View File

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

View 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;
}
}
}
}
}

View File

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

View File

@ -0,0 +1,65 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
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

View File

@ -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));
} }

View File

@ -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>())

View File

@ -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));
} }

View File

@ -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;
}
}
}

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.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" />

View File

@ -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));
} }
} }
} }

View File

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

View File

@ -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()

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -193,30 +193,46 @@ 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)
{ {
new StatisticRow var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
return new[]
{ {
Columns = new[] new StatisticRow
{ {
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList()) Columns = new[]
{ {
RelativeSizeAxes = Axes.X, new StatisticItem("Timing Distribution",
Height = 250 new HitEventTimingDistributionGraph(timedHitEvents)
}), {
} RelativeSizeAxes = Axes.X,
}, Height = 250
new StatisticRow }),
{ }
Columns = new[] },
new StatisticRow
{ {
new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) Columns = new[]
{ {
RelativeSizeAxes = Axes.X, new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap)
Height = 250 {
}), RelativeSizeAxes = Axes.X,
Height = 250
}),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new UnstableRate(timedHitEvents)
}))
}
} }
} };
}; }
} }
} }

View File

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

View File

@ -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);
} }

View File

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

View File

@ -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,65 +35,82 @@ namespace osu.Game.Rulesets.Osu.Skinning
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[] InternalChild = new Container
{ {
new Sprite // the old-style spinner relied heavily on absolute screen-space coordinate values.
// wrap everything in a container simulating absolute coords to preserve alignment
// as there are skins that depend on it.
Width = 640,
Height = 480,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{ {
Anchor = Anchor.BottomCentre, new Sprite
Origin = Anchor.BottomCentre,
Texture = source.GetTexture("spinner-background"),
Y = background_y_offset,
Scale = new Vector2(sprite_scale)
},
disc = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-circle"),
Scale = new Vector2(sprite_scale)
},
metre = new Container
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Y = background_y_offset,
Masking = true,
Child = new Sprite
{ {
Texture = source.GetTexture("spinner-metre"), Anchor = Anchor.Centre,
Anchor = Anchor.BottomCentre, Origin = Anchor.Centre,
Origin = Anchor.BottomCentre, Texture = source.GetTexture("spinner-background"),
Scale = new Vector2(sprite_scale)
}, },
Scale = new Vector2(0.625f) disc = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-circle"),
Scale = new Vector2(sprite_scale)
},
metre = new Container
{
AutoSizeAxes = Axes.Both,
// this anchor makes no sense, but that's what stable uses.
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
// adjustment for stable (metre has additional offset)
Margin = new MarginPadding { Top = 20 },
Masking = true,
Child = metreSprite = new Sprite
{
Texture = source.GetTexture("spinner-metre"),
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Scale = new Vector2(0.625f)
}
}
} }
}; };
} }
private Vector2 metreFinalSize;
protected override void LoadComplete() 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;
} }
} }
} }

View File

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

View File

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

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.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" />

View File

@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
private void updateAccentColour() private void updateAccentColour()
{ {
backgroundLayer.Colour = accentColour; backgroundLayer.Colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour);
} }
} }
} }

View File

@ -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;
} }
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using 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(
? new Color4(235, 69, 44, 255) component == TaikoSkinComponents.CentreHit
: new Color4(67, 142, 172, 255); ? new Color4(235, 69, 44, 255)
: new Color4(67, 142, 172, 255));
} }
} }
} }

View File

@ -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)
{ {
new StatisticRow var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList();
return new[]
{ {
Columns = new[] new StatisticRow
{ {
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList()) Columns = new[]
{ {
RelativeSizeAxes = Axes.X, new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(timedHitEvents)
Height = 250 {
}), RelativeSizeAxes = Axes.X,
Height = 250
}),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new UnstableRate(timedHitEvents)
}))
}
} }
} };
}; }
} }
} }

View 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));
}
}
}

View File

@ -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();
} }
} }
} }

View 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()
}
});
}
}
}

View File

@ -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);

View File

@ -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);
} }

View 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