1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-13 08:32:57 +08:00

Merge branch 'master' into storyboard-skin-sprites

This commit is contained in:
Sebastian Krajewski 2020-10-09 16:55:45 +02:00 committed by GitHub
commit e0b5ead223
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
793 changed files with 18812 additions and 5053 deletions

View File

@ -191,4 +191,7 @@ dotnet_diagnostic.IDE0052.severity = silent
#Rules for disposable
dotnet_diagnostic.IDE0067.severity = none
dotnet_diagnostic.IDE0068.severity = none
dotnet_diagnostic.IDE0069.severity = none
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\**\*.*" />
</ItemGroup>
<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" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.0.0" PrivateAssets="All" />
</ItemGroup>

View File

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

View File

@ -9,7 +9,9 @@
[![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu)
[![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew.
A free-to-win rhythm game. Rhythm is just a *click* away!
The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew.
## Status
@ -36,7 +38,13 @@ If you are looking to install or test osu! without setting up a development envi
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
## Developing or debugging
## Developing a custom ruleset
osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu-templates).
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/issues/5852).
## Developing osu!
Please make sure you have the following prerequisites:

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ using osu.Framework.Android;
namespace osu.Android
{
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
public class OsuGameActivity : AndroidGameActivity
{
protected override Framework.Game CreateGame() => new OsuGameAndroid();

View File

@ -29,6 +29,11 @@ namespace osu.Desktop.Updater
private static readonly Logger logger = Logger.GetLogger("updater");
/// <summary>
/// Whether an update has been downloaded but not yet applied.
/// </summary>
private bool updatePending;
[BackgroundDependencyLoader]
private void load(NotificationOverlay notification)
{
@ -37,9 +42,9 @@ namespace osu.Desktop.Updater
Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
}
protected override async Task PerformUpdateCheck() => await checkForUpdateAsync();
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync();
private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
private async Task<bool> checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
{
// should we schedule a retry on completion of this check?
bool scheduleRecheck = true;
@ -49,9 +54,19 @@ namespace osu.Desktop.Updater
updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true);
var info = await updateManager.CheckForUpdate(!useDeltaPatching);
if (info.ReleasesToApply.Count == 0)
{
if (updatePending)
{
// the user may have dismissed the completion notice, so show it again.
notificationOverlay.Post(new UpdateCompleteNotification(this));
return true;
}
// no updates available. bail and retry later.
return;
return false;
}
if (notification == null)
{
@ -72,6 +87,7 @@ namespace osu.Desktop.Updater
await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f);
notification.State = ProgressNotificationState.Completed;
updatePending = true;
}
catch (Exception e)
{
@ -103,6 +119,8 @@ namespace osu.Desktop.Updater
Scheduler.AddDelayed(async () => await checkForUpdateAsync(), 60000 * 30);
}
}
return true;
}
protected override void Dispose(bool isDisposing)
@ -111,10 +129,27 @@ namespace osu.Desktop.Updater
updateManager?.Dispose();
}
private class UpdateCompleteNotification : ProgressCompletionNotification
{
[Resolved]
private OsuGame game { get; set; }
public UpdateCompleteNotification(SquirrelUpdateManager updateManager)
{
Text = @"Update ready to install. Click to restart!";
Activated = () =>
{
updateManager.PrepareUpdateAsync()
.ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
return true;
};
}
}
private class UpdateProgressNotification : ProgressNotification
{
private readonly SquirrelUpdateManager updateManager;
private OsuGame game;
public UpdateProgressNotification(SquirrelUpdateManager updateManager)
{
@ -123,23 +158,12 @@ namespace osu.Desktop.Updater
protected override Notification CreateCompletionNotification()
{
return new ProgressCompletionNotification
{
Text = @"Update ready to install. Click to restart!",
Activated = () =>
{
updateManager.PrepareUpdateAsync()
.ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
return true;
}
};
return new UpdateCompleteNotification(updateManager);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, OsuGame game)
private void load(OsuColour colours)
{
this.game = game;
IconContent.AddRange(new Drawable[]
{
new Box

View File

@ -3,7 +3,7 @@
<TargetFramework>netcoreapp3.1</TargetFramework>
<OutputType>WinExe</OutputType>
<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>
<Title>osu!lazer</Title>
<Product>osu!lazer</Product>

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

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

View File

@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Catch.Tests
Schedule(() =>
{
area.AttemptCatch(fruit);
area.OnResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great });
area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great });
drawable.Expire();
});

View File

@ -0,0 +1,65 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneComboCounter : CatchSkinnableTestScene
{
private ScoreProcessor scoreProcessor;
private Color4 judgedObjectColour = Color4.White;
[SetUp]
public void SetUp() => Schedule(() =>
{
scoreProcessor = new ScoreProcessor();
SetContents(() => new CatchComboDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(2.5f),
});
});
[Test]
public void TestCatchComboCounter()
{
AddRepeatStep("perform hit", () => performJudgement(HitResult.Great), 20);
AddStep("perform miss", () => performJudgement(HitResult.Miss));
AddStep("randomize judged object colour", () =>
{
judgedObjectColour = new Color4(
RNG.NextSingle(1f),
RNG.NextSingle(1f),
RNG.NextSingle(1f),
1f
);
});
}
private void performJudgement(HitResult type, Judgement judgement = null)
{
var judgedObject = new DrawableFruit(new Fruit()) { AccentColour = { Value = judgedObjectColour } };
var result = new JudgementResult(judgedObject.HitObject, judgement ?? new Judgement()) { Type = type };
scoreProcessor.ApplyResult(result);
foreach (var counter in CreatedDrawables.Cast<CatchComboDisplay>())
counter.OnNewResult(judgedObject, result);
}
}
}

View File

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

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
@ -18,23 +19,43 @@ namespace osu.Game.Rulesets.Catch.Tests
{
protected override bool Autoplay => true;
private int hyperDashCount;
private bool inHyperDash;
[Test]
public void TestHyperDash()
{
AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash);
AddUntilStep("wait for right movement", () => getCatcher().Scale.X > 0); // don't check hyperdashing as it happens too fast.
AddUntilStep("wait for left movement", () => getCatcher().Scale.X < 0);
for (int i = 0; i < 3; i++)
AddStep("reset count", () =>
{
AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing);
AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing);
inHyperDash = false;
hyperDashCount = 0;
// this needs to be done within the frame stable context due to how quickly hyperdash state changes occur.
Player.DrawableRuleset.FrameStableComponents.OnUpdate += d =>
{
var catcher = Player.ChildrenOfType<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)
{
var beatmap = new Beatmap
@ -46,6 +67,8 @@ namespace osu.Game.Rulesets.Catch.Tests
}
};
beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
// Should produce a hyper-dash (edge case test)
beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56, NewCombo = true });
beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308, NewCombo = true });
@ -63,6 +86,20 @@ namespace osu.Game.Rulesets.Catch.Tests
createObjects(() => new Fruit { X = right_x });
createObjects(() => new TestJuiceStream(left_x), 1);
beatmap.ControlPointInfo.Add(startTime, new TimingControlPoint
{
BeatLength = 50
});
createObjects(() => new TestJuiceStream(left_x)
{
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero),
new PathControlPoint(new Vector2(512, 0))
})
}, 1);
return beatmap;
void createObjects(Func<CatchHitObject> createObject, int count = 3)

View File

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

View File

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

View File

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

View File

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

View File

@ -21,13 +21,11 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System;
using osu.Framework.Testing;
using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
{
[ExcludeFromDynamicCompile]
public class CatchRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableCatchRuleset(this, beatmap, mods);
@ -143,11 +141,40 @@ namespace osu.Game.Rulesets.Catch
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch };
protected override IEnumerable<HitResult> GetValidHitResults()
{
return new[]
{
HitResult.Great,
HitResult.LargeTickHit,
HitResult.SmallTickHit,
HitResult.LargeBonus,
};
}
public override string GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{
case HitResult.LargeTickHit:
return "large droplet";
case HitResult.SmallTickHit:
return "small droplet";
case HitResult.LargeBonus:
return "banana";
}
return base.GetDisplayNameForHitResult(result);
}
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new CatchPerformanceCalculator(this, beatmap, score);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score);
public int LegacyID => 2;

View File

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

View File

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

View File

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

View File

@ -8,31 +8,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
{
public class CatchBananaJudgement : CatchJudgement
{
public override bool AffectsCombo => false;
protected override int NumericResultFor(HitResult result)
{
switch (result)
{
default:
return 0;
case HitResult.Perfect:
return 1100;
}
}
protected override double HealthIncreaseFor(HitResult result)
{
switch (result)
{
default:
return 0;
case HitResult.Perfect:
return DEFAULT_MAX_HEALTH_INCREASE * 0.75;
}
}
public override HitResult MaxResult => HitResult.LargeBonus;
public override bool ShouldExplodeFor(JudgementResult result) => true;
}

View File

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

View File

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

View File

@ -7,30 +7,6 @@ namespace osu.Game.Rulesets.Catch.Judgements
{
public class CatchTinyDropletJudgement : CatchJudgement
{
public override bool AffectsCombo => false;
protected override int NumericResultFor(HitResult result)
{
switch (result)
{
default:
return 0;
case HitResult.Perfect:
return 10;
}
}
protected override double HealthIncreaseFor(HitResult result)
{
switch (result)
{
default:
return 0;
case HitResult.Perfect:
return 0.02;
}
}
public override HitResult MaxResult => HitResult.SmallTickHit;
}
}

View File

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

View File

@ -27,6 +27,11 @@ namespace osu.Game.Rulesets.Catch.Objects
set => x = value;
}
/// <summary>
/// Whether this object can be placed on the catcher's plate.
/// </summary>
public virtual bool CanBePlated => false;
/// <summary>
/// A random offset applied to <see cref="X"/>, set by the <see cref="CatchBeatmapProcessor"/>.
/// </summary>
@ -100,6 +105,14 @@ namespace osu.Game.Rulesets.Catch.Objects
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
{
Pear,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,8 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Catch.Skinning
{
@ -51,6 +53,15 @@ namespace osu.Game.Rulesets.Catch.Skinning
case CatchSkinComponents.CatcherKiai:
return this.GetAnimation("fruit-catcher-kiai", true, true, true) ??
this.GetAnimation("fruit-ryuuta", true, true, true);
case CatchSkinComponents.CatchComboCounter:
var comboFont = GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
if (this.HasFont(comboFont))
return new LegacyComboCounter(Source);
break;
}
return null;
@ -61,7 +72,12 @@ namespace osu.Game.Rulesets.Catch.Skinning
switch (lookup)
{
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);

View File

@ -0,0 +1,103 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Catch.Skinning
{
/// <summary>
/// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
/// </summary>
public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter
{
private readonly LegacyRollingCounter counter;
private readonly LegacyRollingCounter explosion;
public LegacyComboCounter(ISkin skin)
{
var fontName = skin.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
var fontOverlap = skin.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? -2f;
AutoSizeAxes = Axes.Both;
Alpha = 0f;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Scale = new Vector2(0.8f);
InternalChildren = new Drawable[]
{
explosion = new LegacyRollingCounter(skin, fontName, fontOverlap)
{
Alpha = 0.65f,
Blending = BlendingParameters.Additive,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(1.5f),
},
counter = new LegacyRollingCounter(skin, fontName, fontOverlap)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
};
}
private int lastDisplayedCombo;
public void UpdateCombo(int combo, Color4? hitObjectColour = null)
{
if (combo == lastDisplayedCombo)
return;
// There may still be existing transforms to the counter (including value change after 250ms),
// finish them immediately before new transforms.
counter.SetCountWithoutRolling(lastDisplayedCombo);
lastDisplayedCombo = combo;
if (Time.Elapsed < 0)
{
// needs more work to make rewind somehow look good.
// basically we want the previous increment to play... or turning off RemoveCompletedTransforms (not feasible from a performance angle).
Hide();
return;
}
// Combo fell to zero, roll down and fade out the counter.
if (combo == 0)
{
counter.Current.Value = 0;
explosion.Current.Value = 0;
this.FadeOut(400, Easing.Out);
}
else
{
this.FadeInFromZero().Then().Delay(1000).FadeOut(300);
counter.ScaleTo(1.5f)
.ScaleTo(0.8f, 250, Easing.Out)
.OnComplete(c => c.SetCountWithoutRolling(combo));
counter.Delay(250)
.ScaleTo(1f)
.ScaleTo(1.1f, 60).Then().ScaleTo(1f, 30);
explosion.Colour = hitObjectColour ?? Color4.White;
explosion.SetCountWithoutRolling(combo);
explosion.ScaleTo(1.5f)
.ScaleTo(1.9f, 400, Easing.Out)
.FadeOutFromOne(400);
}
}
}
}

View File

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

View File

@ -0,0 +1,62 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI
{
/// <summary>
/// Represents a component that displays a skinned <see cref="ICatchComboCounter"/> and handles combo judgement results for updating it accordingly.
/// </summary>
public class CatchComboDisplay : SkinnableDrawable
{
private int currentCombo;
[CanBeNull]
public ICatchComboCounter ComboCounter => Drawable as ICatchComboCounter;
public CatchComboDisplay()
: base(new CatchSkinComponent(CatchSkinComponents.CatchComboCounter), _ => Empty())
{
}
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
ComboCounter?.UpdateCombo(currentCombo);
}
public void OnNewResult(DrawableCatchHitObject judgedObject, JudgementResult result)
{
if (!result.Type.AffectsCombo() || !result.HasResult)
return;
if (!result.IsHit)
{
updateCombo(0, null);
return;
}
updateCombo(result.ComboAtJudgement + 1, judgedObject.AccentColour.Value);
}
public void OnRevertResult(DrawableCatchHitObject judgedObject, JudgementResult result)
{
if (!result.Type.AffectsCombo() || !result.HasResult)
return;
updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value);
}
private void updateCombo(int newCombo, Color4? hitObjectColour)
{
currentCombo = newCombo;
ComboCounter?.UpdateCombo(newCombo, hitObjectColour);
}
}
}

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osuTK;
@ -23,6 +24,7 @@ namespace osu.Game.Rulesets.Catch.UI
public Func<CatchHitObject, DrawableHitObject<CatchHitObject>> CreateDrawableRepresentation;
public readonly Catcher MovableCatcher;
private readonly CatchComboDisplay comboDisplay;
public Container ExplodingFruitTarget
{
@ -34,12 +36,24 @@ namespace osu.Game.Rulesets.Catch.UI
public CatcherArea(BeatmapDifficulty difficulty = null)
{
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
Child = MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X };
Children = new Drawable[]
{
comboDisplay = new CatchComboDisplay
{
RelativeSizeAxes = Axes.None,
AutoSizeAxes = Axes.Both,
Anchor = Anchor.TopLeft,
Origin = Anchor.Centre,
Margin = new MarginPadding { Bottom = 350f },
X = CatchPlayfield.CENTER_X
},
MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X },
};
}
public void OnResult(DrawableCatchHitObject fruit, JudgementResult result)
public void OnNewResult(DrawableCatchHitObject fruit, JudgementResult result)
{
if (result.Judgement is IgnoreJudgement)
if (!result.Type.IsScorable())
return;
void runAfterLoaded(Action action)
@ -55,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.UI
lastPlateableFruit.OnLoadComplete += _ => action();
}
if (result.IsHit && fruit.CanBePlated)
if (result.IsHit && fruit.HitObject.CanBePlated)
{
// create a new (cloned) fruit to stay on the plate. the original is faded out immediately.
var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit.HitObject);
@ -86,8 +100,13 @@ namespace osu.Game.Rulesets.Catch.UI
else
MovableCatcher.Drop();
}
comboDisplay.OnNewResult(fruit, result);
}
public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result)
=> comboDisplay.OnRevertResult(fruit, result);
public void OnReleased(CatchAction action)
{
}
@ -105,6 +124,8 @@ namespace osu.Game.Rulesets.Catch.UI
if (state?.CatcherX != null)
MovableCatcher.X = state.CatcherX.Value;
comboDisplay.X = MovableCatcher.X;
}
}
}

View File

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

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI
{
/// <summary>
/// An interface providing a set of methods to update the combo counter.
/// </summary>
public interface ICatchComboCounter : IDrawable
{
/// <summary>
/// Updates the counter to animate a transition from the old combo value it had to the current provided one.
/// </summary>
/// <remarks>
/// This is called regardless of whether the clock is rewinding.
/// </remarks>
/// <param name="combo">The new combo value.</param>
/// <param name="hitObjectColour">The colour of the object if hit, null on miss.</param>
void UpdateCombo(int combo, Color4? hitObjectColour = null);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,11 +14,13 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests
{
[TestFixture]
[Timeout(10000)]
public class ManiaBeatmapConversionTest : BeatmapConversionTest<ManiaConvertMapping, ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase("basic")]
[TestCase("zero-length-slider")]
public void Test(string name) => base.Test(name);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
@ -81,11 +83,17 @@ namespace osu.Game.Rulesets.Mania.Tests
RandomZ = snapshot.RandomZ;
}
public override void PostProcess()
{
base.PostProcess();
Objects.Sort();
}
public bool Equals(ManiaConvertMapping other) => other != null && RandomW == other.RandomW && RandomX == other.RandomX && RandomY == other.RandomY && RandomZ == other.RandomZ;
public override bool Equals(ConvertMapping<ConvertValue> other) => base.Equals(other) && Equals(other as ManiaConvertMapping);
}
public struct ConvertValue : IEquatable<ConvertValue>
public struct ConvertValue : IEquatable<ConvertValue>, IComparable<ConvertValue>
{
/// <summary>
/// A sane value to account for osu!stable using ints everwhere.
@ -100,5 +108,15 @@ namespace osu.Game.Rulesets.Mania.Tests
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
&& Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
&& Column == other.Column;
public int CompareTo(ConvertValue other)
{
var result = StartTime.CompareTo(other.StartTime);
if (result != 0)
return result;
return Column.CompareTo(other.Column);
}
}
}

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 | LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime), typeof(ManiaModPerfect) })]
[TestCase(LegacyMods.Random | LegacyMods.SuddenDeath, new[] { typeof(ManiaModRandom), typeof(ManiaModSuddenDeath) })]
[TestCase(LegacyMods.Flashlight | LegacyMods.Mirror, new[] { typeof(ManiaModFlashlight), typeof(ManiaModMirror) })]
public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods);
protected override Ruleset CreateRuleset() => new ManiaRuleset();

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]
private readonly Column column;
public ColumnTestContainer(int column, ManiaAction action)
public ColumnTestContainer(int column, ManiaAction action, bool showColumn = false)
{
this.column = new Column(column)
InternalChildren = new[]
{
Action = { Value = action },
AccentColour = Color4.Orange,
ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd
};
InternalChild = content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
{
RelativeSizeAxes = Axes.Both
this.column = new Column(column)
{
Action = { Value = action },
AccentColour = Color4.Orange,
ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd,
Alpha = showColumn ? 1 : 0
},
content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
{
RelativeSizeAxes = Axes.Both
},
this.column.TopLevelContainer.CreateProxy()
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,25 +28,33 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestFixture]
public class TestSceneNotes : OsuTestScene
{
[BackgroundDependencyLoader]
private void load()
[Test]
public void TestVariousNotes()
{
Child = new FillFlowContainer
DrawableNote note1 = null;
DrawableNote note2 = null;
DrawableHoldNote holdNote1 = null;
DrawableHoldNote holdNote2 = null;
AddStep("create notes", () =>
{
Clock = new FramedClock(new ManualClock()),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(20),
Children = new[]
Child = new FillFlowContainer
{
createNoteDisplay(ScrollingDirection.Down, 1, out var note1),
createNoteDisplay(ScrollingDirection.Up, 2, out var note2),
createHoldNoteDisplay(ScrollingDirection.Down, 1, out var holdNote1),
createHoldNoteDisplay(ScrollingDirection.Up, 2, out var holdNote2),
}
};
Clock = new FramedClock(new ManualClock()),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(20),
Children = new[]
{
createNoteDisplay(ScrollingDirection.Down, 1, out note1),
createNoteDisplay(ScrollingDirection.Up, 2, out note2),
createHoldNoteDisplay(ScrollingDirection.Down, 1, out holdNote1),
createHoldNoteDisplay(ScrollingDirection.Up, 2, out holdNote2),
}
};
});
AddAssert("note 1 facing downwards", () => verifyAnchors(note1, Anchor.y2));
AddAssert("note 2 facing upwards", () => verifyAnchors(note2, Anchor.y0));

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" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

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

View File

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

View File

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

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