mirror of
https://github.com/ppy/osu.git
synced 2026-05-24 16:20:37 +08:00
Compare commits
1440 Commits
@@ -14,8 +14,8 @@
|
||||
"jb"
|
||||
]
|
||||
},
|
||||
"smoogipoo.nvika": {
|
||||
"version": "1.0.1",
|
||||
"nvika": {
|
||||
"version": "2.2.0",
|
||||
"commands": [
|
||||
"nvika"
|
||||
]
|
||||
@@ -33,4 +33,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -113,7 +113,7 @@ dotnet_style_qualification_for_event = false:warning
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
|
||||
dotnet_style_predefined_type_for_member_access = true:warning
|
||||
csharp_style_var_when_type_is_apparent = true:none
|
||||
csharp_style_var_for_built_in_types = true:none
|
||||
csharp_style_var_for_built_in_types = false:warning
|
||||
csharp_style_var_elsewhere = true:silent
|
||||
|
||||
#Style - modifiers
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
github: ppy
|
||||
custom: https://osu.ppy.sh/home/support
|
||||
|
||||
@@ -5,7 +5,7 @@ updates:
|
||||
schedule:
|
||||
interval: monthly
|
||||
time: "17:00"
|
||||
open-pull-requests-limit: 99
|
||||
open-pull-requests-limit: 0 # disabled until https://github.com/dependabot/dependabot-core/issues/369 is resolved.
|
||||
ignore:
|
||||
- dependency-name: Microsoft.EntityFrameworkCore.Design
|
||||
versions:
|
||||
|
||||
@@ -50,6 +50,55 @@ jobs:
|
||||
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
|
||||
path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx
|
||||
|
||||
build-only-android:
|
||||
name: Build only (Android)
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Pin Xamarin.Android version to 11.2 for now to avoid build failures caused by a Xamarin-side regression.
|
||||
# See: https://github.com/xamarin/xamarin-android/issues/6284
|
||||
# This can be removed/reverted when the fix makes it to upstream and is deployed on github runners.
|
||||
- name: Set default Xamarin SDK version
|
||||
run: |
|
||||
$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.2
|
||||
|
||||
- name: Install .NET 5.0.x
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: "5.0.x"
|
||||
|
||||
# Contrary to seemingly any other msbuild, msbuild running on macOS/Mono
|
||||
# cannot accept .sln(f) files as arguments.
|
||||
# Build just the main game for now.
|
||||
- name: Build
|
||||
run: msbuild osu.Android/osu.Android.csproj /restore /p:Configuration=Debug
|
||||
|
||||
build-only-ios:
|
||||
# While this workflow technically *can* run, it fails as iOS builds are blocked by multiple issues.
|
||||
# See https://github.com/ppy/osu-framework/issues/4677 for the details.
|
||||
# The job can be unblocked once those issues are resolved and game deployments can happen again.
|
||||
if: false
|
||||
name: Build only (iOS)
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install .NET 5.0.x
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: "5.0.x"
|
||||
|
||||
# Contrary to seemingly any other msbuild, msbuild running on macOS/Mono
|
||||
# cannot accept .sln(f) files as arguments.
|
||||
# Build just the main game for now.
|
||||
- name: Build
|
||||
run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug
|
||||
|
||||
inspect-code:
|
||||
name: Code Quality
|
||||
runs-on: ubuntu-latest
|
||||
@@ -79,9 +128,14 @@ jobs:
|
||||
run: |
|
||||
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
|
||||
# FIXME: Suppress warnings from templates project
|
||||
dotnet codefilesanity | while read -r line; do
|
||||
echo "::warning::$line"
|
||||
done
|
||||
exit_code=0
|
||||
while read -r line; do
|
||||
if [[ ! -z "$line" ]]; then
|
||||
echo "::error::$line"
|
||||
exit_code=1
|
||||
fi
|
||||
done <<< $(dotnet codefilesanity)
|
||||
exit $exit_code
|
||||
|
||||
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
|
||||
# - name: .NET Format (Dry Run)
|
||||
|
||||
@@ -30,3 +30,5 @@ jobs:
|
||||
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
|
||||
path: "*.trx"
|
||||
reporter: dotnet-trx
|
||||
list-suites: 'failed'
|
||||
list-tests: 'failed'
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<EmbeddedResource Include="Resources\**\*.*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Code Analysis">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.2" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3" PrivateAssets="All" />
|
||||
<AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
||||
+2
-2
@@ -3,10 +3,10 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.EmptyFreeform.Replays;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Rulesets.EmptyFreeform.Mods
|
||||
{
|
||||
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Mods
|
||||
{
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
User = new User { Username = "sample" },
|
||||
User = new APIUser { Username = "sample" },
|
||||
},
|
||||
Replay = new EmptyFreeformAutoGenerator(beatmap).Generate(),
|
||||
};
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
||||
+2
-2
@@ -3,10 +3,10 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Pippidon.Replays;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Rulesets.Pippidon.Mods
|
||||
{
|
||||
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Pippidon.Mods
|
||||
{
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
User = new User { Username = "sample" },
|
||||
User = new APIUser { Username = "sample" },
|
||||
},
|
||||
Replay = new PippidonAutoGenerator(beatmap).Generate(),
|
||||
};
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.EmptyScrolling.Replays;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
|
||||
namespace osu.Game.Rulesets.EmptyScrolling.Mods
|
||||
{
|
||||
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Mods
|
||||
{
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
User = new User { Username = "sample" },
|
||||
User = new APIUser { Username = "sample" },
|
||||
},
|
||||
Replay = new EmptyScrollingAutoGenerator(beatmap).Generate(),
|
||||
};
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
||||
+2
-2
@@ -3,10 +3,10 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Pippidon.Replays;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Rulesets.Pippidon.Mods
|
||||
{
|
||||
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Pippidon.Mods
|
||||
{
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
User = new User { Username = "sample" },
|
||||
User = new APIUser { Username = "sample" },
|
||||
},
|
||||
Replay = new PippidonAutoGenerator(beatmap).Generate(),
|
||||
};
|
||||
|
||||
+2
-2
@@ -51,8 +51,8 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1004.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1112.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1108.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace osu.Android
|
||||
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-beatmap-archive")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-skin-archive")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-replay")]
|
||||
|
||||
@@ -11,10 +11,10 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Users;
|
||||
using LogLevel = osu.Framework.Logging.LogLevel;
|
||||
using User = osu.Game.Users.User;
|
||||
|
||||
namespace osu.Desktop
|
||||
{
|
||||
@@ -27,7 +27,7 @@ namespace osu.Desktop
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; }
|
||||
|
||||
private IBindable<User> user;
|
||||
private IBindable<APIUser> user;
|
||||
|
||||
private readonly IBindable<UserStatus> status = new Bindable<UserStatus>();
|
||||
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
|
||||
|
||||
@@ -156,7 +156,7 @@ namespace osu.Desktop
|
||||
{
|
||||
lock (importableFiles)
|
||||
{
|
||||
var firstExtension = Path.GetExtension(filePaths.First());
|
||||
string firstExtension = Path.GetExtension(filePaths.First());
|
||||
|
||||
if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
|
||||
|
||||
@@ -177,7 +177,7 @@ namespace osu.Desktop
|
||||
{
|
||||
Logger.Log($"Handling batch import of {importableFiles.Count} files");
|
||||
|
||||
var paths = importableFiles.ToArray();
|
||||
string[] paths = importableFiles.ToArray();
|
||||
importableFiles.Clear();
|
||||
|
||||
Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning);
|
||||
|
||||
@@ -22,17 +22,17 @@ namespace osu.Desktop
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
// Back up the cwd before DesktopGameHost changes it
|
||||
var cwd = Environment.CurrentDirectory;
|
||||
string cwd = Environment.CurrentDirectory;
|
||||
|
||||
string gameName = base_game_name;
|
||||
bool tournamentClient = false;
|
||||
|
||||
foreach (var arg in args)
|
||||
foreach (string arg in args)
|
||||
{
|
||||
var split = arg.Split('=');
|
||||
string[] split = arg.Split('=');
|
||||
|
||||
var key = split[0];
|
||||
var val = split.Length > 1 ? split[1] : string.Empty;
|
||||
string key = split[0];
|
||||
string val = split.Length > 1 ? split[1] : string.Empty;
|
||||
|
||||
switch (key)
|
||||
{
|
||||
@@ -62,7 +62,7 @@ namespace osu.Desktop
|
||||
{
|
||||
var importer = new ArchiveImportIPCChannel(host);
|
||||
|
||||
foreach (var file in args)
|
||||
foreach (string file in args)
|
||||
{
|
||||
Console.WriteLine(@"Importing {0}", file);
|
||||
if (!importer.ImportAsync(Path.GetFullPath(file, cwd)).Wait(3000))
|
||||
|
||||
@@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
|
||||
|
||||
[TestCase(4.050601681491468d, "diffcalc-test")]
|
||||
[TestCase(4.0505463516206195d, "diffcalc-test")]
|
||||
public void Test(double expected, string name)
|
||||
=> base.Test(expected, name);
|
||||
|
||||
[TestCase(5.169743871843191d, "diffcalc-test")]
|
||||
[TestCase(5.1696411260785498d, "diffcalc-test")]
|
||||
public void TestClockRateAdjusted(double expected, string name)
|
||||
=> Test(expected, name, new CatchModDoubleTime());
|
||||
|
||||
|
||||
@@ -29,8 +29,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
|
||||
protected CatchSelectionBlueprintTestScene()
|
||||
{
|
||||
EditorBeatmap = new EditorBeatmap(new CatchBeatmap());
|
||||
EditorBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = 0;
|
||||
EditorBeatmap = new EditorBeatmap(new CatchBeatmap()) { Difficulty = { CircleSize = 0 } };
|
||||
EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint
|
||||
{
|
||||
BeatLength = 100
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Edit.Checks;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests.Editor.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestCheckBananaShowerGap
|
||||
{
|
||||
private CheckBananaShowerGap check;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckBananaShowerGap();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAllowedSpinnerGaps()
|
||||
{
|
||||
assertOk(mockBeatmap(250, 1000, 1250), DifficultyRating.Easy);
|
||||
assertOk(mockBeatmap(250, 1000, 1250), DifficultyRating.Normal);
|
||||
assertOk(mockBeatmap(125, 1000, 1250), DifficultyRating.Hard);
|
||||
assertOk(mockBeatmap(125, 1000, 1125), DifficultyRating.Insane);
|
||||
assertOk(mockBeatmap(62, 1000, 1125), DifficultyRating.Expert);
|
||||
assertOk(mockBeatmap(62, 1000, 1125), DifficultyRating.ExpertPlus);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisallowedSpinnerGapStart()
|
||||
{
|
||||
assertTooShortSpinnerStart(mockBeatmap(249, 1000, 1250), DifficultyRating.Easy);
|
||||
assertTooShortSpinnerStart(mockBeatmap(249, 1000, 1250), DifficultyRating.Normal);
|
||||
assertTooShortSpinnerStart(mockBeatmap(124, 1000, 1250), DifficultyRating.Hard);
|
||||
assertTooShortSpinnerStart(mockBeatmap(124, 1000, 1250), DifficultyRating.Insane);
|
||||
assertTooShortSpinnerStart(mockBeatmap(61, 1000, 1250), DifficultyRating.Expert);
|
||||
assertTooShortSpinnerStart(mockBeatmap(61, 1000, 1250), DifficultyRating.ExpertPlus);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisallowedSpinnerGapEnd()
|
||||
{
|
||||
assertTooShortSpinnerEnd(mockBeatmap(250, 1000, 1249), DifficultyRating.Easy);
|
||||
assertTooShortSpinnerEnd(mockBeatmap(250, 1000, 1249), DifficultyRating.Normal);
|
||||
assertTooShortSpinnerEnd(mockBeatmap(125, 1000, 1249), DifficultyRating.Hard);
|
||||
assertTooShortSpinnerEnd(mockBeatmap(125, 1000, 1124), DifficultyRating.Insane);
|
||||
assertTooShortSpinnerEnd(mockBeatmap(62, 1000, 1124), DifficultyRating.Expert);
|
||||
assertTooShortSpinnerEnd(mockBeatmap(62, 1000, 1124), DifficultyRating.ExpertPlus);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestConsecutiveSpinners()
|
||||
{
|
||||
var spinnerConsecutiveBeatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new BananaShower { StartTime = 0, EndTime = 100, X = 0 },
|
||||
new BananaShower { StartTime = 101, EndTime = 200, X = 0 },
|
||||
new BananaShower { StartTime = 201, EndTime = 300, X = 0 }
|
||||
}
|
||||
};
|
||||
|
||||
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Easy);
|
||||
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Normal);
|
||||
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Hard);
|
||||
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Insane);
|
||||
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Expert);
|
||||
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.ExpertPlus);
|
||||
}
|
||||
|
||||
private Beatmap<HitObject> mockBeatmap(double bananaShowerStart, double bananaShowerEnd, double nextFruitStart)
|
||||
{
|
||||
return new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Fruit { StartTime = 0, X = 0 },
|
||||
new BananaShower { StartTime = bananaShowerStart, EndTime = bananaShowerEnd, X = 0 },
|
||||
new Fruit { StartTime = nextFruitStart, X = 0 }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void assertOk(IBeatmap beatmap, DifficultyRating difficultyRating)
|
||||
{
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
|
||||
Assert.That(check.Run(context), Is.Empty);
|
||||
}
|
||||
|
||||
private void assertTooShortSpinnerStart(IBeatmap beatmap, DifficultyRating difficultyRating)
|
||||
{
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckBananaShowerGap.IssueTemplateBananaShowerStartGap));
|
||||
}
|
||||
|
||||
private void assertTooShortSpinnerEnd(IBeatmap beatmap, DifficultyRating difficultyRating)
|
||||
{
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckBananaShowerGap.IssueTemplateBananaShowerEndGap));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
@@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
protected override void AddHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
// Create nested bananas (but positions are not randomized because beatmap processing is not done).
|
||||
hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), Beatmap.Value.BeatmapInfo.BaseDifficulty);
|
||||
hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
base.AddHitObject(hitObject);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// 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.Input.Events;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Edit;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
public class TestSceneCatchDistanceSnapGrid : OsuManualInputManagerTestScene
|
||||
{
|
||||
private readonly ManualClock manualClock = new ManualClock();
|
||||
|
||||
[Cached(typeof(Playfield))]
|
||||
private readonly CatchPlayfield playfield;
|
||||
|
||||
private ScrollingHitObjectContainer hitObjectContainer => playfield.HitObjectContainer;
|
||||
|
||||
private readonly CatchDistanceSnapGrid distanceGrid;
|
||||
|
||||
private readonly FruitOutline fruitOutline;
|
||||
|
||||
private readonly Fruit fruit = new Fruit();
|
||||
|
||||
public TestSceneCatchDistanceSnapGrid()
|
||||
{
|
||||
Child = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = 500,
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ScrollingTestContainer(ScrollingDirection.Down)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = playfield = new CatchPlayfield(new BeatmapDifficulty())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Clock = new FramedClock(manualClock)
|
||||
}
|
||||
},
|
||||
distanceGrid = new CatchDistanceSnapGrid(new double[] { 0, -1, 1 }),
|
||||
fruitOutline = new FruitOutline()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
distanceGrid.StartTime = 100;
|
||||
distanceGrid.StartX = 250;
|
||||
|
||||
Vector2 screenSpacePosition = InputManager.CurrentState.Mouse.Position;
|
||||
|
||||
var result = distanceGrid.GetSnappedPosition(screenSpacePosition);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
fruit.OriginalX = hitObjectContainer.ToLocalSpace(result.ScreenSpacePosition).X;
|
||||
|
||||
if (result.Time != null)
|
||||
fruit.StartTime = result.Time.Value;
|
||||
}
|
||||
|
||||
fruitOutline.Position = CatchHitObjectUtils.GetStartPosition(hitObjectContainer, fruit);
|
||||
fruitOutline.UpdateFrom(fruit);
|
||||
}
|
||||
|
||||
protected override bool OnScroll(ScrollEvent e)
|
||||
{
|
||||
manualClock.CurrentTime -= e.ScrollDelta.Y * 50;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
@@ -23,11 +23,12 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
|
||||
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
protected override IBeatmap GetPlayableBeatmap()
|
||||
{
|
||||
Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderTickRate = 5;
|
||||
Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity * 10;
|
||||
var playable = base.GetPlayableBeatmap();
|
||||
playable.Difficulty.SliderTickRate = 5;
|
||||
playable.Difficulty.SliderMultiplier = velocity * 10;
|
||||
return playable;
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
|
||||
AddAssert("correct outline count", () =>
|
||||
{
|
||||
var expected = hitObject.NestedHitObjects.Count(h => !(h is TinyDroplet));
|
||||
int expected = hitObject.NestedHitObjects.Count(h => !(h is TinyDroplet));
|
||||
return this.ChildrenOfType<FruitOutline>().Count() == expected;
|
||||
});
|
||||
AddAssert("correct vertex piece count", () =>
|
||||
@@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
X = x,
|
||||
Path = sliderPath,
|
||||
};
|
||||
EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity;
|
||||
EditorBeatmap.Difficulty.SliderMultiplier = velocity;
|
||||
EditorBeatmap.Add(hitObject);
|
||||
EditorBeatmap.Update(hitObject);
|
||||
Assert.That(hitObject.Velocity, Is.EqualTo(velocity));
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
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.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests.Mods
|
||||
{
|
||||
public class TestSceneCatchModNoScope : ModTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestVisibleDuringBreak()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new CatchModNoScope
|
||||
{
|
||||
HiddenComboCount = { Value = 0 },
|
||||
},
|
||||
Autoplay = true,
|
||||
PassCondition = () => true,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Fruit
|
||||
{
|
||||
X = CatchPlayfield.CENTER_X,
|
||||
StartTime = 1000,
|
||||
},
|
||||
new Fruit
|
||||
{
|
||||
X = CatchPlayfield.CENTER_X,
|
||||
StartTime = 5000,
|
||||
}
|
||||
},
|
||||
Breaks = new List<BreakPeriod>
|
||||
{
|
||||
new BreakPeriod(2000, 4000),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddUntilStep("wait for catcher to hide", () => catcherAlphaAlmostEquals(0));
|
||||
AddUntilStep("wait for start of break", isBreak);
|
||||
AddUntilStep("wait for catcher to show", () => catcherAlphaAlmostEquals(1));
|
||||
AddUntilStep("wait for end of break", () => !isBreak());
|
||||
AddUntilStep("wait for catcher to hide", () => catcherAlphaAlmostEquals(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVisibleAfterComboBreak()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new CatchModNoScope
|
||||
{
|
||||
HiddenComboCount = { Value = 2 },
|
||||
},
|
||||
Autoplay = true,
|
||||
PassCondition = () => true,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Fruit
|
||||
{
|
||||
X = 0,
|
||||
StartTime = 1000,
|
||||
},
|
||||
new Fruit
|
||||
{
|
||||
X = CatchPlayfield.CENTER_X,
|
||||
StartTime = 3000,
|
||||
},
|
||||
new Fruit
|
||||
{
|
||||
X = CatchPlayfield.WIDTH,
|
||||
StartTime = 5000,
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddAssert("catcher must start visible", () => catcherAlphaAlmostEquals(1));
|
||||
AddUntilStep("wait for combo", () => Player.ScoreProcessor.Combo.Value >= 2);
|
||||
AddAssert("catcher must dim after combo", () => !catcherAlphaAlmostEquals(1));
|
||||
AddStep("break combo", () => Player.ScoreProcessor.Combo.Value = 0);
|
||||
AddUntilStep("wait for catcher to show", () => catcherAlphaAlmostEquals(1));
|
||||
}
|
||||
|
||||
private bool isBreak() => Player.IsBreakTime.Value;
|
||||
|
||||
private bool catcherAlphaAlmostEquals(float alpha)
|
||||
{
|
||||
var playfield = (CatchPlayfield)Player.DrawableRuleset.Playfield;
|
||||
return Precision.AlmostEquals(playfield.CatcherArea.Alpha, alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[Test]
|
||||
public void TestCatcherCatchWidth()
|
||||
{
|
||||
var halfWidth = Catcher.CalculateCatchWidth(new BeatmapDifficulty { CircleSize = 0 }) / 2;
|
||||
float halfWidth = Catcher.CalculateCatchWidth(new BeatmapDifficulty { CircleSize = 0 }) / 2;
|
||||
AddStep("catch fruit", () =>
|
||||
{
|
||||
attemptCatch(new Fruit { X = -halfWidth + 1 });
|
||||
@@ -237,7 +237,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private void attemptCatch(Func<CatchHitObject> hitObject, int count)
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
for (int i = 0; i < count; i++)
|
||||
attemptCatch(hitObject(), out _, out _);
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private void spawnJuiceStream(bool hit = false)
|
||||
{
|
||||
var xCoords = getXCoords(hit);
|
||||
float xCoords = getXCoords(hit);
|
||||
|
||||
var juice = new JuiceStream
|
||||
{
|
||||
|
||||
@@ -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.11.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
||||
@@ -211,7 +211,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
|
||||
palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
|
||||
|
||||
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2;
|
||||
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) / 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.
|
||||
@@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
int thisDirection = nextObject.EffectiveX > currentObject.EffectiveX ? 1 : -1;
|
||||
double timeToNext = nextObject.StartTime - currentObject.StartTime - 1000f / 60f / 4; // 1/4th of a frame of grace time, taken from osu-stable
|
||||
double distanceToNext = Math.Abs(nextObject.EffectiveX - currentObject.EffectiveX) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth);
|
||||
float distanceToHyper = (float)(timeToNext * Catcher.BASE_SPEED - distanceToNext);
|
||||
float distanceToHyper = (float)(timeToNext * Catcher.BASE_DASH_SPEED - distanceToNext);
|
||||
|
||||
if (distanceToHyper < 0)
|
||||
{
|
||||
|
||||
@@ -133,6 +133,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
new MultiMod(new ModWindUp(), new ModWindDown()),
|
||||
new CatchModFloatingFruits(),
|
||||
new CatchModMuted(),
|
||||
new CatchModNoScope(),
|
||||
};
|
||||
|
||||
default:
|
||||
@@ -188,5 +189,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
||||
|
||||
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
return new CatchDifficultyAttributes { Mods = mods, Skills = skills };
|
||||
|
||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
return new CatchDifficultyAttributes
|
||||
{
|
||||
@@ -69,10 +69,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
{
|
||||
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
|
||||
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f;
|
||||
|
||||
// For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay.
|
||||
halfCatcherWidth *= 1 - (Math.Max(0, beatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5.5f) * 0.0625f);
|
||||
halfCatcherWidth *= 1 - (Math.Max(0, beatmap.Difficulty.CircleSize - 5.5f) * 0.0625f);
|
||||
|
||||
return new Skill[]
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
|
||||
: base(hitObject, lastObject, clockRate)
|
||||
{
|
||||
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||
var scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
|
||||
float scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
|
||||
|
||||
NormalizedPosition = BaseObject.EffectiveX * scalingFactor;
|
||||
LastNormalizedPosition = LastObject.EffectiveX * scalingFactor;
|
||||
|
||||
@@ -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 System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Catch.Edit.Checks;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class CatchBeatmapVerifier : IBeatmapVerifier
|
||||
{
|
||||
private readonly List<ICheck> checks = new List<ICheck>
|
||||
{
|
||||
new CheckBananaShowerGap()
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
return checks.SelectMany(check => check.Run(context));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// 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 JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Lines;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
/// <summary>
|
||||
/// The guide lines used in the osu!catch editor to compose patterns that can be caught with constant speed.
|
||||
/// Currently, only forward placement (an object is snapped based on the previous object, not the opposite) is supported.
|
||||
/// </summary>
|
||||
public class CatchDistanceSnapGrid : CompositeDrawable
|
||||
{
|
||||
public double StartTime { get; set; }
|
||||
|
||||
public float StartX { get; set; }
|
||||
|
||||
private const double max_vertical_line_length_in_time = CatchPlayfield.WIDTH / Catcher.BASE_WALK_SPEED;
|
||||
|
||||
private readonly double[] velocities;
|
||||
|
||||
private readonly List<Path> verticalPaths = new List<Path>();
|
||||
|
||||
private readonly List<Vector2[]> verticalLineVertices = new List<Vector2[]>();
|
||||
|
||||
[Resolved]
|
||||
private Playfield playfield { get; set; }
|
||||
|
||||
private ScrollingHitObjectContainer hitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
|
||||
|
||||
public CatchDistanceSnapGrid(double[] velocities)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Anchor = Anchor.BottomLeft;
|
||||
|
||||
this.velocities = velocities;
|
||||
|
||||
for (int i = 0; i < velocities.Length; i++)
|
||||
{
|
||||
verticalPaths.Add(new SmoothPath
|
||||
{
|
||||
PathRadius = 2,
|
||||
Alpha = 0.5f,
|
||||
});
|
||||
|
||||
verticalLineVertices.Add(new[] { Vector2.Zero, Vector2.Zero });
|
||||
}
|
||||
|
||||
AddRangeInternal(verticalPaths);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
double currentTime = hitObjectContainer.Time.Current;
|
||||
|
||||
for (int i = 0; i < velocities.Length; i++)
|
||||
{
|
||||
double velocity = velocities[i];
|
||||
|
||||
// The line ends at the top of the playfield.
|
||||
double endTime = hitObjectContainer.TimeAtPosition(-hitObjectContainer.DrawHeight, currentTime);
|
||||
|
||||
// Non-vertical lines are cut at the sides of the playfield.
|
||||
// Vertical lines are cut at some reasonable length.
|
||||
if (velocity > 0)
|
||||
endTime = Math.Min(endTime, StartTime + (CatchPlayfield.WIDTH - StartX) / velocity);
|
||||
else if (velocity < 0)
|
||||
endTime = Math.Min(endTime, StartTime + StartX / -velocity);
|
||||
else
|
||||
endTime = Math.Min(endTime, StartTime + max_vertical_line_length_in_time);
|
||||
|
||||
Vector2[] lineVertices = verticalLineVertices[i];
|
||||
lineVertices[0] = calculatePosition(velocity, StartTime);
|
||||
lineVertices[1] = calculatePosition(velocity, endTime);
|
||||
|
||||
var verticalPath = verticalPaths[i];
|
||||
verticalPath.Vertices = verticalLineVertices[i];
|
||||
verticalPath.OriginPosition = verticalPath.PositionInBoundingBox(Vector2.Zero);
|
||||
}
|
||||
|
||||
Vector2 calculatePosition(double velocity, double time)
|
||||
{
|
||||
// Don't draw inverted lines.
|
||||
time = Math.Max(time, StartTime);
|
||||
|
||||
float x = StartX + (float)((time - StartTime) * velocity);
|
||||
float y = hitObjectContainer.PositionAtTime(time, currentTime);
|
||||
return new Vector2(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
public SnapResult GetSnappedPosition(Vector2 screenSpacePosition)
|
||||
{
|
||||
double time = hitObjectContainer.TimeAtScreenSpacePosition(screenSpacePosition);
|
||||
|
||||
// If the cursor is below the distance snap grid, snap to the origin.
|
||||
// Not returning `null` to retain the continuous snapping behavior when the cursor is slightly below the origin.
|
||||
// This behavior is not currently visible in the editor because editor chooses the snap start time based on the mouse position.
|
||||
if (time <= StartTime)
|
||||
{
|
||||
float y = hitObjectContainer.PositionAtTime(StartTime);
|
||||
Vector2 originPosition = hitObjectContainer.ToScreenSpace(new Vector2(StartX, y));
|
||||
return new SnapResult(originPosition, StartTime);
|
||||
}
|
||||
|
||||
return enumerateSnappingCandidates(time)
|
||||
.OrderBy(pos => Vector2.DistanceSquared(screenSpacePosition, pos.ScreenSpacePosition))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private IEnumerable<SnapResult> enumerateSnappingCandidates(double time)
|
||||
{
|
||||
float y = hitObjectContainer.PositionAtTime(time);
|
||||
|
||||
foreach (double velocity in velocities)
|
||||
{
|
||||
float x = (float)(StartX + (time - StartTime) * velocity);
|
||||
Vector2 screenSpacePosition = hitObjectContainer.ToScreenSpace(new Vector2(x, y + hitObjectContainer.DrawHeight));
|
||||
yield return new SnapResult(screenSpacePosition, time);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,23 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
@@ -17,6 +26,14 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class CatchHitObjectComposer : HitObjectComposer<CatchHitObject>
|
||||
{
|
||||
private const float distance_snap_radius = 50;
|
||||
|
||||
private CatchDistanceSnapGrid distanceSnapGrid;
|
||||
|
||||
private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
|
||||
|
||||
private InputManager inputManager;
|
||||
|
||||
public CatchHitObjectComposer(CatchRuleset ruleset)
|
||||
: base(ruleset)
|
||||
{
|
||||
@@ -30,6 +47,27 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
|
||||
});
|
||||
|
||||
LayerBelowRuleset.Add(distanceSnapGrid = new CatchDistanceSnapGrid(new[]
|
||||
{
|
||||
0.0,
|
||||
Catcher.BASE_DASH_SPEED, -Catcher.BASE_DASH_SPEED,
|
||||
Catcher.BASE_WALK_SPEED, -Catcher.BASE_WALK_SPEED,
|
||||
}));
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
inputManager = GetContainingInputManager();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
updateDistanceSnapGrid();
|
||||
}
|
||||
|
||||
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) =>
|
||||
@@ -42,14 +80,95 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
new BananaShowerCompositionTool()
|
||||
};
|
||||
|
||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
|
||||
{
|
||||
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
|
||||
});
|
||||
|
||||
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
|
||||
{
|
||||
var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
|
||||
// TODO: implement position snap
|
||||
result.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||
|
||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
||||
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
|
||||
{
|
||||
result = snapResult;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this);
|
||||
|
||||
[CanBeNull]
|
||||
private PalpableCatchHitObject getLastSnappableHitObject(double time)
|
||||
{
|
||||
var hitObject = EditorBeatmap.HitObjects.OfType<CatchHitObject>().LastOrDefault(h => h.GetEndTime() < time && !(h is BananaShower));
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
case Fruit fruit:
|
||||
return fruit;
|
||||
|
||||
case JuiceStream juiceStream:
|
||||
return juiceStream.NestedHitObjects.OfType<PalpableCatchHitObject>().LastOrDefault(h => !(h is TinyDroplet));
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
private PalpableCatchHitObject getDistanceSnapGridSourceHitObject()
|
||||
{
|
||||
switch (BlueprintContainer.CurrentTool)
|
||||
{
|
||||
case SelectTool _:
|
||||
if (EditorBeatmap.SelectedHitObjects.Count == 0)
|
||||
return null;
|
||||
|
||||
double minTime = EditorBeatmap.SelectedHitObjects.Min(hitObject => hitObject.StartTime);
|
||||
return getLastSnappableHitObject(minTime);
|
||||
|
||||
case FruitCompositionTool _:
|
||||
case JuiceStreamCompositionTool _:
|
||||
if (!CursorInPlacementArea)
|
||||
return null;
|
||||
|
||||
if (EditorBeatmap.PlacementObject.Value is JuiceStream)
|
||||
{
|
||||
// Juice stream path is not subject to snapping.
|
||||
return null;
|
||||
}
|
||||
|
||||
double timeAtCursor = ((CatchPlayfield)Playfield).TimeAtScreenSpacePosition(inputManager.CurrentState.Mouse.Position);
|
||||
return getLastSnappableHitObject(timeAtCursor);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDistanceSnapGrid()
|
||||
{
|
||||
if (distanceSnapToggle.Value != TernaryState.True)
|
||||
{
|
||||
distanceSnapGrid.Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
var sourceHitObject = getDistanceSnapGridSourceHitObject();
|
||||
|
||||
if (sourceHitObject == null)
|
||||
{
|
||||
distanceSnapGrid.Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
distanceSnapGrid.Show();
|
||||
distanceSnapGrid.StartTime = sourceHitObject.GetEndTime();
|
||||
distanceSnapGrid.StartX = sourceHitObject.EffectiveX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Checks
|
||||
{
|
||||
/// <summary>
|
||||
/// Check the spinner/banana shower gaps specified in the osu!catch difficulty specific ranking criteria.
|
||||
/// </summary>
|
||||
public class CheckBananaShowerGap : ICheck
|
||||
{
|
||||
private static readonly Dictionary<DifficultyRating, (int startGap, int endGap)> spinner_delta_threshold = new Dictionary<DifficultyRating, (int, int)>
|
||||
{
|
||||
[DifficultyRating.Easy] = (250, 250),
|
||||
[DifficultyRating.Normal] = (250, 250),
|
||||
[DifficultyRating.Hard] = (125, 250),
|
||||
[DifficultyRating.Insane] = (125, 125),
|
||||
[DifficultyRating.Expert] = (62, 125),
|
||||
[DifficultyRating.ExpertPlus] = (62, 125)
|
||||
};
|
||||
|
||||
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Too short spinner gap");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateBananaShowerStartGap(this),
|
||||
new IssueTemplateBananaShowerEndGap(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
var hitObjects = context.Beatmap.HitObjects;
|
||||
(int expectedStartDelta, int expectedEndDelta) = spinner_delta_threshold[context.InterpretedDifficulty];
|
||||
|
||||
for (int i = 0; i < hitObjects.Count - 1; ++i)
|
||||
{
|
||||
if (!(hitObjects[i] is BananaShower bananaShower))
|
||||
continue;
|
||||
|
||||
// Skip if the previous hitobject is a banana shower, consecutive spinners are allowed
|
||||
if (i != 0 && hitObjects[i - 1] is CatchHitObject previousHitObject && !(previousHitObject is BananaShower))
|
||||
{
|
||||
double spinnerStartDelta = bananaShower.StartTime - previousHitObject.GetEndTime();
|
||||
|
||||
if (spinnerStartDelta < expectedStartDelta)
|
||||
{
|
||||
yield return new IssueTemplateBananaShowerStartGap(this)
|
||||
.Create(spinnerStartDelta, expectedStartDelta, bananaShower, previousHitObject);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if the next hitobject is a banana shower, consecutive spinners are allowed
|
||||
if (hitObjects[i + 1] is CatchHitObject nextHitObject && !(nextHitObject is BananaShower))
|
||||
{
|
||||
double spinnerEndDelta = nextHitObject.StartTime - bananaShower.EndTime;
|
||||
|
||||
if (spinnerEndDelta < expectedEndDelta)
|
||||
{
|
||||
yield return new IssueTemplateBananaShowerEndGap(this)
|
||||
.Create(spinnerEndDelta, expectedEndDelta, bananaShower, nextHitObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class IssueTemplateBananaShowerGap : IssueTemplate
|
||||
{
|
||||
protected IssueTemplateBananaShowerGap(ICheck check, IssueType issueType, string unformattedMessage)
|
||||
: base(check, issueType, unformattedMessage)
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(double deltaTime, int expectedDeltaTime, params HitObject[] hitObjects)
|
||||
{
|
||||
return new Issue(hitObjects, this, Math.Floor(deltaTime), expectedDeltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateBananaShowerStartGap : IssueTemplateBananaShowerGap
|
||||
{
|
||||
public IssueTemplateBananaShowerStartGap(ICheck check)
|
||||
: base(check, IssueType.Problem, "There is only {0} ms between the start of the spinner and the last object, it should not be less than {1} ms.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateBananaShowerEndGap : IssueTemplateBananaShowerGap
|
||||
{
|
||||
public IssueTemplateBananaShowerEndGap(ICheck check)
|
||||
: base(check, IssueType.Problem, "There is only {0} ms between the end of the spinner and the next object, it should not be less than {1} ms.")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
}
|
||||
|
||||
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
|
||||
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.Difficulty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
|
||||
{
|
||||
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
|
||||
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!salad" } },
|
||||
Replay = new CatchAutoGenerator(beatmap).Generate(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
|
||||
{
|
||||
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
|
||||
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!salad" } },
|
||||
Replay = new CatchAutoGenerator(beatmap).Generate(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
var hitObject = drawable.HitObject;
|
||||
|
||||
var offset = hitObject.TimePreempt * fade_out_offset_multiplier;
|
||||
var duration = offset - hitObject.TimePreempt * fade_out_duration_multiplier;
|
||||
double offset = hitObject.TimePreempt * fade_out_offset_multiplier;
|
||||
double duration = offset - hitObject.TimePreempt * fade_out_duration_multiplier;
|
||||
|
||||
using (drawable.BeginAbsoluteSequence(hitObject.StartTime - offset))
|
||||
drawable.FadeOut(duration);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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 osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModNoScope : ModNoScope, IUpdatableByPlayfield
|
||||
{
|
||||
public override string Description => "Where's the catcher?";
|
||||
|
||||
[SettingSource(
|
||||
"Hidden at combo",
|
||||
"The combo count at which the catcher becomes completely hidden",
|
||||
SettingControlType = typeof(SettingsSlider<int, HiddenComboSlider>)
|
||||
)]
|
||||
public override BindableInt HiddenComboCount { get; } = new BindableInt
|
||||
{
|
||||
Default = 10,
|
||||
Value = 10,
|
||||
MinValue = 0,
|
||||
MaxValue = 50,
|
||||
};
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
var catchPlayfield = (CatchPlayfield)playfield;
|
||||
bool shouldAlwaysShowCatcher = IsBreakTime.Value;
|
||||
float targetAlpha = shouldAlwaysShowCatcher ? 1 : ComboBasedAlpha;
|
||||
catchPlayfield.CatcherArea.Alpha = (float)Interpolation.Lerp(catchPlayfield.CatcherArea.Alpha, targetAlpha, Math.Clamp(catchPlayfield.Time.Elapsed / TRANSITION_DURATION, 0, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,9 +42,8 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||
DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
|
||||
|
||||
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
|
||||
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
|
||||
|
||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||
TickDistance = scoringDistance / difficulty.SliderTickRate;
|
||||
@@ -146,7 +145,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
public double Distance => Path.Distance;
|
||||
|
||||
public List<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
|
||||
public IList<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
|
||||
|
||||
public double? LegacyLastTickOffset { get; set; }
|
||||
}
|
||||
|
||||
@@ -26,9 +26,6 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
if (Beatmap.HitObjects.Count == 0)
|
||||
return;
|
||||
|
||||
// todo: add support for HT DT
|
||||
const double dash_speed = Catcher.BASE_SPEED;
|
||||
const double movement_speed = dash_speed / 2;
|
||||
float lastPosition = CatchPlayfield.CENTER_X;
|
||||
double lastTime = 0;
|
||||
|
||||
@@ -47,8 +44,8 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
// The case where positionChange > 0 and timeAvailable == 0 results in PositiveInfinity which provides expected beheaviour.
|
||||
double speedRequired = positionChange == 0 ? 0 : positionChange / timeAvailable;
|
||||
|
||||
bool dashRequired = speedRequired > movement_speed;
|
||||
bool impossibleJump = speedRequired > movement_speed * 2;
|
||||
bool dashRequired = speedRequired > Catcher.BASE_WALK_SPEED;
|
||||
bool impossibleJump = speedRequired > Catcher.BASE_DASH_SPEED;
|
||||
|
||||
// todo: get correct catcher size, based on difficulty CS.
|
||||
const float catcher_width_half = Catcher.BASE_SIZE * 0.3f * 0.5f;
|
||||
@@ -73,7 +70,7 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
else if (dashRequired)
|
||||
{
|
||||
// we do a movement in two parts - the dash part then the normal part...
|
||||
double timeAtNormalSpeed = positionChange / movement_speed;
|
||||
double timeAtNormalSpeed = positionChange / Catcher.BASE_WALK_SPEED;
|
||||
double timeWeNeedToSave = timeAtNormalSpeed - timeAvailable;
|
||||
double timeAtDashSpeed = timeWeNeedToSave / 2;
|
||||
|
||||
@@ -86,7 +83,7 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
}
|
||||
else
|
||||
{
|
||||
double timeBefore = positionChange / movement_speed;
|
||||
double timeBefore = positionChange / Catcher.BASE_WALK_SPEED;
|
||||
|
||||
addFrame(h.StartTime - timeBefore, lastPosition);
|
||||
addFrame(h.StartTime, h.EffectiveX);
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
|
||||
public override void CollectPendingInputs(List<IInput> inputs)
|
||||
{
|
||||
var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
|
||||
float position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
|
||||
|
||||
inputs.Add(new CatchReplayState
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
return null;
|
||||
|
||||
case CatchSkinComponents.Catcher:
|
||||
var version = GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1;
|
||||
decimal version = GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value ?? 1;
|
||||
|
||||
if (version < 2.3m)
|
||||
{
|
||||
|
||||
@@ -57,14 +57,19 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
public bool CatchFruitOnPlate { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
|
||||
/// The speed of the catcher when the catcher is dashing.
|
||||
/// </summary>
|
||||
public const double BASE_SPEED = 1.0;
|
||||
public const double BASE_DASH_SPEED = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// The current speed of the catcher.
|
||||
/// The speed of the catcher when the catcher is not dashing.
|
||||
/// </summary>
|
||||
public double Speed => (Dashing ? 1 : 0.5) * BASE_SPEED * hyperDashModifier;
|
||||
public const double BASE_WALK_SPEED = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// The current speed of the catcher with the hyper-dash modifier applied.
|
||||
/// </summary>
|
||||
public double Speed => (Dashing ? BASE_DASH_SPEED : BASE_WALK_SPEED) * hyperDashModifier;
|
||||
|
||||
/// <summary>
|
||||
/// The amount by which caught fruit should be scaled down to fit on the plate.
|
||||
@@ -226,11 +231,11 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
if (result.IsHit && hitObject.HyperDash)
|
||||
{
|
||||
var target = hitObject.HyperDashTarget;
|
||||
var timeDifference = target.StartTime - hitObject.StartTime;
|
||||
double timeDifference = target.StartTime - hitObject.StartTime;
|
||||
double positionDifference = target.EffectiveX - X;
|
||||
var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
|
||||
double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
|
||||
|
||||
SetHyperDashState(Math.Abs(velocity), target.EffectiveX);
|
||||
SetHyperDashState(Math.Abs(velocity) / BASE_DASH_SPEED, target.EffectiveX);
|
||||
}
|
||||
else
|
||||
SetHyperDashState();
|
||||
@@ -266,7 +271,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// <param name="targetPosition">When this catcher crosses this position, this catcher ends hyper-dashing.</param>
|
||||
public void SetHyperDashState(double modifier = 1, float targetPosition = -1)
|
||||
{
|
||||
var wasHyperDashing = HyperDashing;
|
||||
bool wasHyperDashing = HyperDashing;
|
||||
|
||||
if (modifier <= 1 || X == targetPosition)
|
||||
{
|
||||
|
||||
@@ -27,14 +27,14 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
Direction.Value = ScrollingDirection.Down;
|
||||
TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450);
|
||||
TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450);
|
||||
}
|
||||
|
||||
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
|
||||
|
||||
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);
|
||||
|
||||
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
|
||||
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.Difficulty);
|
||||
|
||||
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer();
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
|
||||
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
|
||||
{
|
||||
var time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
|
||||
double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
|
||||
var pos = column.ScreenSpacePositionAtTime(time);
|
||||
|
||||
return new SnapResult(pos, time, column);
|
||||
|
||||
@@ -13,6 +13,7 @@ using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Edit;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
@@ -101,27 +102,27 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override float GetBeatSnapDistanceAt(double referenceTime)
|
||||
public override float GetBeatSnapDistanceAt(HitObject referenceObject)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override float DurationToDistance(double referenceTime, double duration)
|
||||
public override float DurationToDistance(HitObject referenceObject, double duration)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override double DistanceToDuration(double referenceTime, float distance)
|
||||
public override double DistanceToDuration(HitObject referenceObject, float distance)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override double GetSnappedDurationFromDistance(double referenceTime, float distance)
|
||||
public override double GetSnappedDurationFromDistance(HitObject referenceObject, float distance)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override float GetSnappedDistanceFromDistance(double referenceTime, float distance)
|
||||
public override float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
[Resolved]
|
||||
private SkinManager skins { get; set; }
|
||||
|
||||
[Cached]
|
||||
private EditorClipboard clipboard = new EditorClipboard();
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
public int CompareTo(ConvertValue other)
|
||||
{
|
||||
var result = StartTime.CompareTo(other.StartTime);
|
||||
int result = StartTime.CompareTo(other.StartTime);
|
||||
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
private IList<string> getSampleNames(IList<HitSampleInfo> hitSampleInfo)
|
||||
=> hitSampleInfo.Select(sample => sample.LookupNames.First()).ToList();
|
||||
|
||||
private IList<IList<string>> getNodeSampleNames(List<IList<HitSampleInfo>> hitSampleInfo)
|
||||
private IList<IList<string>> getNodeSampleNames(IList<IList<HitSampleInfo>> hitSampleInfo)
|
||||
=> hitSampleInfo?.Select(getSampleNames)
|
||||
.ToList();
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
private IEnumerable<ColumnType> getResults(StageDefinition definition)
|
||||
{
|
||||
for (var i = 0; i < definition.Columns; i++)
|
||||
for (int i = 0; i < definition.Columns; i++)
|
||||
yield return definition.GetTypeOfColumn(i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,7 +388,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
},
|
||||
};
|
||||
|
||||
beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
|
||||
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
|
||||
}
|
||||
|
||||
AddStep("load player", () =>
|
||||
|
||||
@@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
},
|
||||
});
|
||||
|
||||
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
|
||||
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
|
||||
@@ -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.11.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
||||
@@ -42,8 +42,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
{
|
||||
IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
|
||||
|
||||
var roundedCircleSize = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
|
||||
var roundedOverallDifficulty = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
double roundedCircleSize = Math.Round(beatmap.Difficulty.CircleSize);
|
||||
double roundedOverallDifficulty = Math.Round(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
if (IsForCurrentRuleset)
|
||||
{
|
||||
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
|
||||
public static int GetColumnCountForNonConvert(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
var roundedCircleSize = Math.Round(beatmapInfo.BaseDifficulty.CircleSize);
|
||||
double roundedCircleSize = Math.Round(beatmapInfo.BaseDifficulty.CircleSize);
|
||||
return (int)Math.Max(1, roundedCircleSize);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
|
||||
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
|
||||
{
|
||||
IBeatmapDifficultyInfo difficulty = original.BeatmapInfo.BaseDifficulty;
|
||||
IBeatmapDifficultyInfo difficulty = original.Difficulty;
|
||||
|
||||
int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
|
||||
Random = new FastRandom(seed);
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
Debug.Assert(distanceData != null);
|
||||
|
||||
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
|
||||
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
|
||||
DifficultyControlPoint difficultyPoint = hitObject.DifficultyControlPoint;
|
||||
|
||||
double beatLength;
|
||||
#pragma warning disable 618
|
||||
@@ -55,13 +55,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
#pragma warning restore 618
|
||||
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
|
||||
else
|
||||
beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
|
||||
beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity;
|
||||
|
||||
SpanCount = repeatsData?.SpanCount() ?? 1;
|
||||
StartTime = (int)Math.Round(hitObject.StartTime);
|
||||
|
||||
// This matches stable's calculation.
|
||||
EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier);
|
||||
EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.Difficulty.SliderMultiplier);
|
||||
|
||||
SegmentDuration = (EndTime - StartTime) / SpanCount;
|
||||
}
|
||||
@@ -488,12 +488,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
/// Retrieves the list of node samples that occur at time greater than or equal to <paramref name="time"/>.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to retrieve node samples at.</param>
|
||||
private List<IList<HitSampleInfo>> nodeSamplesAt(int time)
|
||||
private IList<IList<HitSampleInfo>> nodeSamplesAt(int time)
|
||||
{
|
||||
if (!(HitObject is IHasPathWithRepeats curveData))
|
||||
return null;
|
||||
|
||||
var index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;
|
||||
int index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;
|
||||
|
||||
// avoid slicing the list & creating copies, if at all possible.
|
||||
return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList();
|
||||
|
||||
@@ -302,7 +302,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
|
||||
var pattern = new Pattern();
|
||||
|
||||
int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out var addToCentre);
|
||||
int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out bool addToCentre);
|
||||
|
||||
int columnLimit = (TotalColumns % 2 == 0 ? TotalColumns : TotalColumns - 1) / 2;
|
||||
int nextColumn = GetRandomColumn(upperBound: columnLimit);
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
if (drainTime == 0)
|
||||
drainTime = 10000;
|
||||
|
||||
IBeatmapDifficultyInfo difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty;
|
||||
IBeatmapDifficultyInfo difficulty = OriginalBeatmap.Difficulty;
|
||||
conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
|
||||
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
|
||||
|
||||
|
||||
@@ -28,7 +28,12 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
|
||||
{
|
||||
new TrackedSetting<double>(ManiaRulesetSetting.ScrollTime,
|
||||
v => new SettingDescription(v, "Scroll Speed", $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / v)} ({v}ms)"))
|
||||
scrollTime => new SettingDescription(
|
||||
rawValue: scrollTime,
|
||||
name: "Scroll Speed",
|
||||
value: $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)} ({scrollTime}ms)"
|
||||
)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -41,14 +41,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
return new ManiaDifficultyAttributes { Mods = mods, Skills = skills };
|
||||
|
||||
HitWindows hitWindows = new ManiaHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
return new ManiaDifficultyAttributes
|
||||
{
|
||||
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
|
||||
Mods = mods,
|
||||
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
||||
GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate),
|
||||
GreatHitWindow = Math.Ceiling(getHitWindow300(mods) / clockRate),
|
||||
ScoreMultiplier = getScoreMultiplier(mods),
|
||||
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
|
||||
Skills = skills
|
||||
|
||||
@@ -35,8 +35,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||
var endTime = maniaCurrent.EndTime;
|
||||
var column = maniaCurrent.BaseObject.Column;
|
||||
double endTime = maniaCurrent.EndTime;
|
||||
int column = maniaCurrent.BaseObject.Column;
|
||||
|
||||
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
|
||||
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
|
||||
|
||||
@@ -316,7 +316,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
case PlayfieldType.Dual:
|
||||
{
|
||||
var keys = getDualStageKeyCount(variant);
|
||||
int keys = getDualStageKeyCount(variant);
|
||||
return $"{keys}K + {keys}K";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +62,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils
|
||||
|
||||
if (i < j)
|
||||
{
|
||||
T key = keys[i];
|
||||
keys[i] = keys[j];
|
||||
keys[j] = key;
|
||||
(keys[i], keys[j]) = (keys[j], keys[i]);
|
||||
}
|
||||
|
||||
i++;
|
||||
@@ -122,7 +120,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils
|
||||
|
||||
while (i <= n / 2)
|
||||
{
|
||||
var child = 2 * i;
|
||||
int child = 2 * i;
|
||||
|
||||
if (child < n && comparer.Compare(keys[lo + child - 1], keys[lo + child]) < 0)
|
||||
{
|
||||
@@ -142,11 +140,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils
|
||||
private static void swap(T[] a, int i, int j)
|
||||
{
|
||||
if (i != j)
|
||||
{
|
||||
T t = a[i];
|
||||
a[i] = a[j];
|
||||
a[j] = t;
|
||||
}
|
||||
(a[i], a[j]) = (a[j], a[i]);
|
||||
}
|
||||
|
||||
private static void swapIfGreater(T[] keys, IComparer<T> comparer, int a, int b)
|
||||
@@ -154,11 +148,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils
|
||||
if (a != b)
|
||||
{
|
||||
if (comparer.Compare(keys[a], keys[b]) > 0)
|
||||
{
|
||||
T key = keys[a];
|
||||
keys[a] = keys[b];
|
||||
keys[b] = key;
|
||||
}
|
||||
(keys[a], keys[b]) = (keys[b], keys[a]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
|
||||
{
|
||||
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
|
||||
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!topus" } },
|
||||
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
|
||||
{
|
||||
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
|
||||
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!topus" } },
|
||||
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
var availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
|
||||
int availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
|
||||
|
||||
beatmap.HitObjects.OfType<ManiaHitObject>().ForEach(h => h.Column = availableColumns - 1 - h.Column);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
Seed.Value ??= RNG.Next();
|
||||
var rng = new Random((int)Seed.Value);
|
||||
|
||||
var availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
|
||||
int availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
|
||||
var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(item => rng.Next()).ToList();
|
||||
|
||||
beatmap.HitObjects.OfType<ManiaHitObject>().ForEach(h => h.Column = shuffledColumns[h.Column]);
|
||||
|
||||
@@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
if (Time.Current < HitObject.StartTime)
|
||||
return;
|
||||
|
||||
var startTime = holdStartTime?.Invoke();
|
||||
double? startTime = holdStartTime?.Invoke();
|
||||
|
||||
if (startTime == null || startTime > HitObject.StartTime)
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
}
|
||||
}
|
||||
|
||||
public List<IList<HitSampleInfo>> NodeSamples { get; set; }
|
||||
public IList<IList<HitSampleInfo>> NodeSamples { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The head note of the hold.
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Mania.Replays
|
||||
{
|
||||
var currentObject = Beatmap.HitObjects[i];
|
||||
var nextObjectInColumn = GetNextObject(i); // Get the next object that requires pressing the same button
|
||||
var releaseTime = calculateReleaseTime(currentObject, nextObjectInColumn);
|
||||
double releaseTime = calculateReleaseTime(currentObject, nextObjectInColumn);
|
||||
|
||||
yield return new HitPoint { Time = currentObject.StartTime, Column = currentObject.Column };
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ SliderTickRate:1
|
||||
|
||||
[TimingPoints]
|
||||
0,500,4,1,0,100,1,0
|
||||
10000,-150,4,1,0,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
51,192,500,128,0,1500:1:0:0:0:
|
||||
|
||||
@@ -86,20 +86,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = foregroundBuffer = new BufferedContainer
|
||||
InternalChild = foregroundBuffer = new BufferedContainer(cachedFrameBuffer: true)
|
||||
{
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CacheDrawnFrameBuffer = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box { RelativeSizeAxes = Axes.Both },
|
||||
subtractionBuffer = new BufferedContainer
|
||||
subtractionBuffer = new BufferedContainer(cachedFrameBuffer: true)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
// This is needed because we're blending with another object
|
||||
BackgroundColour = Color4.White.Opacity(0),
|
||||
CacheDrawnFrameBuffer = true,
|
||||
// The 'hole' is achieved by subtracting the result of this container with the parent
|
||||
Blending = new BlendingParameters { AlphaEquation = BlendingEquation.ReverseSubtract },
|
||||
Child = subtractionLayer = new CircularContainer
|
||||
|
||||
@@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
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
|
||||
bool hasRightLine = rightLineWidth > 0 && skin.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value >= 2.4m
|
||||
|| isLastColumn;
|
||||
|
||||
Color4 lineColour = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.ColumnLineColour, columnIndex)?.Value ?? Color4.White;
|
||||
|
||||
@@ -63,10 +63,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
{
|
||||
this.beatmap = (ManiaBeatmap)beatmap;
|
||||
|
||||
isLegacySkin = new Lazy<bool>(() => GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null);
|
||||
isLegacySkin = new Lazy<bool>(() => GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version) != null);
|
||||
hasKeyTexture = new Lazy<bool>(() =>
|
||||
{
|
||||
var keyImage = this.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1";
|
||||
string keyImage = this.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1";
|
||||
return this.GetAnimation(keyImage, true, true) != null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,11 +90,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
// Mania doesn't care about global velocity
|
||||
p.Velocity = 1;
|
||||
p.BaseBeatLength *= Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier;
|
||||
p.BaseBeatLength *= Beatmap.Difficulty.SliderMultiplier;
|
||||
|
||||
// For non-mania beatmap, speed changes should only happen through timing points
|
||||
if (!isForCurrentRuleset)
|
||||
p.DifficultyPoint = new DifficultyControlPoint();
|
||||
p.EffectPoint = new EffectControlPoint();
|
||||
}
|
||||
|
||||
BarLines.ForEach(Playfield.Add);
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Edit;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@@ -45,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
|
||||
editorBeatmap.Difficulty.SliderMultiplier = 1;
|
||||
editorBeatmap.ControlPointInfo.Clear();
|
||||
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
|
||||
|
||||
@@ -179,15 +180,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
|
||||
|
||||
public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;
|
||||
public float GetBeatSnapDistanceAt(HitObject referenceObject) => (float)beat_length;
|
||||
|
||||
public float DurationToDistance(double referenceTime, double duration) => (float)duration;
|
||||
public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;
|
||||
|
||||
public double DistanceToDuration(double referenceTime, float distance) => distance;
|
||||
public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
|
||||
|
||||
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
|
||||
public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0;
|
||||
|
||||
public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
|
||||
public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// 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.Utils;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public class TestSceneSliderStreamConversion : TestSceneOsuEditor
|
||||
{
|
||||
private BindableBeatDivisor beatDivisor => (BindableBeatDivisor)Editor.Dependencies.Get(typeof(BindableBeatDivisor));
|
||||
|
||||
[Test]
|
||||
public void TestSimpleConversion()
|
||||
{
|
||||
Slider slider = null;
|
||||
|
||||
AddStep("select first slider", () =>
|
||||
{
|
||||
slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
|
||||
EditorClock.Seek(slider.StartTime);
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
});
|
||||
|
||||
convertToStream();
|
||||
|
||||
AddAssert("stream created", () => streamCreatedFor(slider,
|
||||
(time: 0, pathPosition: 0),
|
||||
(time: 0.25, pathPosition: 0.25),
|
||||
(time: 0.5, pathPosition: 0.5),
|
||||
(time: 0.75, pathPosition: 0.75),
|
||||
(time: 1, pathPosition: 1)));
|
||||
|
||||
AddStep("undo", () => Editor.Undo());
|
||||
AddAssert("slider restored", () => sliderRestored(slider));
|
||||
|
||||
AddStep("select first slider", () =>
|
||||
{
|
||||
slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
|
||||
EditorClock.Seek(slider.StartTime);
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
});
|
||||
AddStep("change beat divisor", () => beatDivisor.Value = 8);
|
||||
|
||||
convertToStream();
|
||||
AddAssert("stream created", () => streamCreatedFor(slider,
|
||||
(time: 0, pathPosition: 0),
|
||||
(time: 0.125, pathPosition: 0.125),
|
||||
(time: 0.25, pathPosition: 0.25),
|
||||
(time: 0.375, pathPosition: 0.375),
|
||||
(time: 0.5, pathPosition: 0.5),
|
||||
(time: 0.625, pathPosition: 0.625),
|
||||
(time: 0.75, pathPosition: 0.75),
|
||||
(time: 0.875, pathPosition: 0.875),
|
||||
(time: 1, pathPosition: 1)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestConversionWithNonMatchingDivisor()
|
||||
{
|
||||
Slider slider = null;
|
||||
|
||||
AddStep("select second slider", () =>
|
||||
{
|
||||
slider = (Slider)EditorBeatmap.HitObjects.Where(h => h is Slider).ElementAt(1);
|
||||
EditorClock.Seek(slider.StartTime);
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
});
|
||||
AddStep("change beat divisor", () => beatDivisor.Value = 3);
|
||||
|
||||
convertToStream();
|
||||
|
||||
AddAssert("stream created", () => streamCreatedFor(slider,
|
||||
(time: 0, pathPosition: 0),
|
||||
(time: 2 / 3d, pathPosition: 2 / 3d)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestConversionWithRepeats()
|
||||
{
|
||||
Slider slider = null;
|
||||
|
||||
AddStep("select first slider with repeats", () =>
|
||||
{
|
||||
slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider s && s.RepeatCount > 0);
|
||||
EditorClock.Seek(slider.StartTime);
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
});
|
||||
AddStep("change beat divisor", () => beatDivisor.Value = 2);
|
||||
|
||||
convertToStream();
|
||||
|
||||
AddAssert("stream created", () => streamCreatedFor(slider,
|
||||
(time: 0, pathPosition: 0),
|
||||
(time: 0.25, pathPosition: 0.5),
|
||||
(time: 0.5, pathPosition: 1),
|
||||
(time: 0.75, pathPosition: 0.5),
|
||||
(time: 1, pathPosition: 0)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestConversionPreservesSliderProperties()
|
||||
{
|
||||
Slider slider = null;
|
||||
|
||||
AddStep("select second new-combo-starting slider", () =>
|
||||
{
|
||||
slider = (Slider)EditorBeatmap.HitObjects.Where(h => h is Slider s && s.NewCombo).ElementAt(1);
|
||||
EditorClock.Seek(slider.StartTime);
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
});
|
||||
|
||||
convertToStream();
|
||||
|
||||
AddAssert("stream created", () => streamCreatedFor(slider,
|
||||
(time: 0, pathPosition: 0),
|
||||
(time: 0.25, pathPosition: 0.25),
|
||||
(time: 0.5, pathPosition: 0.5),
|
||||
(time: 0.75, pathPosition: 0.75),
|
||||
(time: 1, pathPosition: 1)));
|
||||
|
||||
AddStep("undo", () => Editor.Undo());
|
||||
AddAssert("slider restored", () => sliderRestored(slider));
|
||||
}
|
||||
|
||||
private void convertToStream()
|
||||
{
|
||||
AddStep("convert to stream", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LControl);
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.Key(Key.F);
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
InputManager.ReleaseKey(Key.LControl);
|
||||
});
|
||||
}
|
||||
|
||||
private bool streamCreatedFor(Slider slider, params (double time, double pathPosition)[] expectedCircles)
|
||||
{
|
||||
if (EditorBeatmap.HitObjects.Contains(slider))
|
||||
return false;
|
||||
|
||||
foreach ((double expectedTime, double expectedPathPosition) in expectedCircles)
|
||||
{
|
||||
double time = slider.StartTime + slider.Duration * expectedTime;
|
||||
Vector2 position = slider.Position + slider.Path.PositionAt(expectedPathPosition);
|
||||
|
||||
if (!EditorBeatmap.HitObjects.OfType<HitCircle>().Any(h => matches(h, time, position, slider.NewCombo && expectedTime == 0)))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
bool matches(HitCircle circle, double time, Vector2 position, bool startsNewCombo) =>
|
||||
Precision.AlmostEquals(circle.StartTime, time, 1)
|
||||
&& Precision.AlmostEquals(circle.Position, position, 0.01f)
|
||||
&& circle.NewCombo == startsNewCombo
|
||||
&& circle.Samples.SequenceEqual(slider.HeadCircle.Samples)
|
||||
&& circle.SampleControlPoint.IsRedundant(slider.SampleControlPoint);
|
||||
}
|
||||
|
||||
private bool sliderRestored(Slider slider)
|
||||
{
|
||||
var objects = EditorBeatmap.HitObjects.Where(h => h.StartTime >= slider.StartTime && h.GetEndTime() <= slider.EndTime).ToList();
|
||||
|
||||
if (objects.Count > 1)
|
||||
return false;
|
||||
|
||||
var hitObject = objects.Single();
|
||||
if (!(hitObject is Slider restoredSlider))
|
||||
return false;
|
||||
|
||||
return Precision.AlmostEquals(slider.StartTime, restoredSlider.StartTime)
|
||||
&& Precision.AlmostEquals(slider.GetEndTime(), restoredSlider.GetEndTime())
|
||||
&& Precision.AlmostEquals(slider.Position, restoredSlider.Position, 0.01f)
|
||||
&& Precision.AlmostEquals(slider.EndPosition, restoredSlider.EndPosition, 0.01f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,8 +45,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
new Spinner
|
||||
{
|
||||
Duration = 2000,
|
||||
Position = OsuPlayfield.BASE_SIZE / 2
|
||||
Duration = 6000,
|
||||
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -117,10 +117,46 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
PassCondition = checkSomeHit
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestApproachCirclesOnly() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModHidden { OnlyFadeApproachCircles = { Value = true } },
|
||||
Autoplay = true,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = 1000,
|
||||
Position = new Vector2(206, 142)
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = 2000,
|
||||
Position = new Vector2(306, 142)
|
||||
},
|
||||
new Slider
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = new Vector2(156, 242),
|
||||
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(200, 0), })
|
||||
},
|
||||
new Spinner
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
StartTime = 7000,
|
||||
EndTime = 9000
|
||||
}
|
||||
}
|
||||
},
|
||||
PassCondition = checkSomeHit
|
||||
});
|
||||
|
||||
private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4;
|
||||
|
||||
private bool objectWithIncreasedVisibilityHasIndex(int index)
|
||||
=> Player.Mods.Value.OfType<TestOsuModHidden>().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index];
|
||||
=> Player.GameplayState.Mods.OfType<TestOsuModHidden>().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index];
|
||||
|
||||
private class TestOsuModHidden : OsuModHidden
|
||||
{
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// 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.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
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 osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public class TestSceneOsuModNoScope : OsuModTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestVisibleDuringBreak()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModNoScope
|
||||
{
|
||||
HiddenComboCount = { Value = 0 },
|
||||
},
|
||||
Autoplay = true,
|
||||
PassCondition = () => true,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
Position = new Vector2(300, 192),
|
||||
StartTime = 1000,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = new Vector2(300, 192),
|
||||
StartTime = 5000,
|
||||
}
|
||||
},
|
||||
Breaks = new List<BreakPeriod>
|
||||
{
|
||||
new BreakPeriod(2000, 4000),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddUntilStep("wait for cursor to hide", () => cursorAlphaAlmostEquals(0));
|
||||
AddUntilStep("wait for start of break", isBreak);
|
||||
AddUntilStep("wait for cursor to show", () => cursorAlphaAlmostEquals(1));
|
||||
AddUntilStep("wait for end of break", () => !isBreak());
|
||||
AddUntilStep("wait for cursor to hide", () => cursorAlphaAlmostEquals(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVisibleDuringSpinner()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModNoScope
|
||||
{
|
||||
HiddenComboCount = { Value = 0 },
|
||||
},
|
||||
Autoplay = true,
|
||||
PassCondition = () => true,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
Position = new Vector2(300, 192),
|
||||
StartTime = 1000,
|
||||
},
|
||||
new Spinner
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
StartTime = 2000,
|
||||
Duration = 2000,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = new Vector2(300, 192),
|
||||
StartTime = 5000,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddUntilStep("wait for cursor to hide", () => cursorAlphaAlmostEquals(0));
|
||||
AddUntilStep("wait for start of spinner", isSpinning);
|
||||
AddUntilStep("wait for cursor to show", () => cursorAlphaAlmostEquals(1));
|
||||
AddUntilStep("wait for end of spinner", () => !isSpinning());
|
||||
AddUntilStep("wait for cursor to hide", () => cursorAlphaAlmostEquals(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVisibleAfterComboBreak()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModNoScope
|
||||
{
|
||||
HiddenComboCount = { Value = 2 },
|
||||
},
|
||||
Autoplay = true,
|
||||
PassCondition = () => true,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
Position = new Vector2(100, 192),
|
||||
StartTime = 1000,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = new Vector2(150, 192),
|
||||
StartTime = 3000,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = new Vector2(200, 192),
|
||||
StartTime = 5000,
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddAssert("cursor must start visible", () => cursorAlphaAlmostEquals(1));
|
||||
AddUntilStep("wait for combo", () => Player.ScoreProcessor.Combo.Value >= 2);
|
||||
AddAssert("cursor must dim after combo", () => !cursorAlphaAlmostEquals(1));
|
||||
AddStep("break combo", () => Player.ScoreProcessor.Combo.Value = 0);
|
||||
AddUntilStep("wait for cursor to show", () => cursorAlphaAlmostEquals(1));
|
||||
}
|
||||
|
||||
private bool isSpinning() => Player.ChildrenOfType<DrawableSpinner>().SingleOrDefault()?.Progress > 0;
|
||||
|
||||
private bool isBreak() => Player.IsBreakTime.Value;
|
||||
|
||||
private bool cursorAlphaAlmostEquals(float alpha) => Precision.AlmostEquals(Player.DrawableRuleset.Cursor.Alpha, alpha);
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
|
||||
|
||||
[TestCase(6.6634445062299665d, "diffcalc-test")]
|
||||
[TestCase(1.0414203870195022d, "zero-length-sliders")]
|
||||
[TestCase(6.6972307565739273d, "diffcalc-test")]
|
||||
[TestCase(1.4484754139145539d, "zero-length-sliders")]
|
||||
public void Test(double expected, string name)
|
||||
=> base.Test(expected, name);
|
||||
|
||||
[TestCase(8.3858089051603368d, "diffcalc-test")]
|
||||
[TestCase(1.2723279173428435d, "zero-length-sliders")]
|
||||
[TestCase(8.9382559208689809d, "diffcalc-test")]
|
||||
[TestCase(1.7548875851757628d, "zero-length-sliders")]
|
||||
public void TestClockRateAdjusted(double expected, string name)
|
||||
=> Test(expected, name, new OsuModDoubleTime());
|
||||
|
||||
|
||||
@@ -59,8 +59,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddSliderStep("circle size", 0f, 10f, 0f, val =>
|
||||
{
|
||||
config.SetValue(OsuSetting.AutoCursorSize, true);
|
||||
gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = val;
|
||||
Scheduler.AddOnce(() => loadContent(false));
|
||||
gameplayState.Beatmap.Difficulty.CircleSize = val;
|
||||
Scheduler.AddOnce(loadContent);
|
||||
});
|
||||
|
||||
AddStep("test cursor container", () => loadContent(false));
|
||||
@@ -75,10 +75,10 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
public void TestSizing(int circleSize, float userScale)
|
||||
{
|
||||
AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale));
|
||||
AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize);
|
||||
AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.Difficulty.CircleSize = circleSize);
|
||||
AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true));
|
||||
|
||||
AddStep("load content", () => loadContent());
|
||||
AddStep("load content", loadContent);
|
||||
|
||||
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);
|
||||
|
||||
@@ -98,7 +98,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddStep("load content", () => loadContent(false, () => new SkinProvidingContainer(new TopLeftCursorSkin())));
|
||||
}
|
||||
|
||||
private void loadContent(bool automated = true, Func<SkinProvidingContainer> skinProvider = null)
|
||||
private void loadContent() => loadContent(false);
|
||||
|
||||
private void loadContent(bool automated, Func<SkinProvidingContainer> skinProvider = null)
|
||||
{
|
||||
SetContents(_ =>
|
||||
{
|
||||
|
||||
@@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
private void scheduleHit() => AddStep("schedule action", () =>
|
||||
{
|
||||
var delay = hitCircle.StartTime - hitCircle.HitWindows.WindowFor(HitResult.Great) - Time.Current;
|
||||
double delay = hitCircle.StartTime - hitCircle.HitWindows.WindowFor(HitResult.Great) - Time.Current;
|
||||
Scheduler.AddDelayed(() => hitAreaReceptor.OnPressed(new KeyBindingPressEvent<OsuAction>(GetContainingInputManager().CurrentState, OsuAction.LeftButton)), delay);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
@@ -14,7 +15,6 @@ using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
};
|
||||
|
||||
var hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
@@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
};
|
||||
|
||||
var hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
@@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
|
||||
{
|
||||
ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } },
|
||||
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "Autoplay" } },
|
||||
Replay = new MissingAutoGenerator(beatmap, mods).Generate()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneNoSpinnerStacking : TestSceneOsuPlayer
|
||||
{
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
|
||||
{
|
||||
var beatmap = new Beatmap
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
BaseDifficulty = new BeatmapDifficulty { OverallDifficulty = 10 },
|
||||
Ruleset = ruleset
|
||||
}
|
||||
};
|
||||
|
||||
for (int i = 0; i < 512; i++)
|
||||
{
|
||||
if (i % 32 < 20)
|
||||
beatmap.HitObjects.Add(new Spinner { Position = new Vector2(256, 192), StartTime = i * 200, EndTime = (i * 200) + 100 });
|
||||
}
|
||||
|
||||
return beatmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -400,15 +400,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
|
||||
{
|
||||
HitObjects = hitObjects,
|
||||
Difficulty = new BeatmapDifficulty { SliderTickRate = 3 },
|
||||
BeatmapInfo =
|
||||
{
|
||||
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
|
||||
Ruleset = new OsuRuleset().RulesetInfo
|
||||
},
|
||||
});
|
||||
|
||||
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
|
||||
|
||||
SelectedMods.Value = new[] { new OsuModClassic() };
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
@@ -439,6 +437,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public TestSlider()
|
||||
{
|
||||
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
|
||||
|
||||
DefaultsApplied += _ =>
|
||||
{
|
||||
HeadCircle.HitWindows = new TestHitWindows();
|
||||
|
||||
@@ -13,6 +13,7 @@ using osuTK.Graphics;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
@@ -328,10 +329,14 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier)
|
||||
{
|
||||
var cpi = new ControlPointInfo();
|
||||
cpi.Add(0, new DifficultyControlPoint { SpeedMultiplier = speedMultiplier });
|
||||
var cpi = new LegacyControlPointInfo();
|
||||
cpi.Add(0, new DifficultyControlPoint { SliderVelocity = speedMultiplier });
|
||||
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 });
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty
|
||||
{
|
||||
CircleSize = circleSize,
|
||||
SliderTickRate = 3
|
||||
});
|
||||
|
||||
var drawable = CreateDrawableSlider(slider);
|
||||
|
||||
|
||||
@@ -348,6 +348,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
StartTime = time_slider_start,
|
||||
Position = new Vector2(0, 0),
|
||||
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f },
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
@@ -362,8 +363,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
},
|
||||
});
|
||||
|
||||
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
p.OnLoadComplete += _ =>
|
||||
|
||||
@@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public class TestSceneSpinnerRotation : TestSceneOsuPlayer
|
||||
{
|
||||
private const double spinner_start_time = 100;
|
||||
private const double spinner_duration = 6000;
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audioManager { get; set; }
|
||||
|
||||
@@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
double finalTrackerRotation = 0, trackerRotationTolerance = 0;
|
||||
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
|
||||
|
||||
addSeekStep(5000);
|
||||
addSeekStep(spinner_start_time + 5000);
|
||||
AddStep("retrieve disc rotation", () =>
|
||||
{
|
||||
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
|
||||
@@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
});
|
||||
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
|
||||
|
||||
addSeekStep(2500);
|
||||
addSeekStep(spinner_start_time + 2500);
|
||||
AddAssert("disc rotation rewound",
|
||||
// we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
|
||||
// due to the exponential damping applied we're allowing a larger margin of error of about 10%
|
||||
@@ -102,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
|
||||
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
|
||||
|
||||
addSeekStep(5000);
|
||||
addSeekStep(spinner_start_time + 5000);
|
||||
AddAssert("is disc rotation almost same",
|
||||
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
|
||||
AddAssert("is symbol rotation almost same",
|
||||
@@ -140,12 +143,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[Test]
|
||||
public void TestSpinnerNormalBonusRewinding()
|
||||
{
|
||||
addSeekStep(1000);
|
||||
addSeekStep(spinner_start_time + 1000);
|
||||
|
||||
AddAssert("player score matching expected bonus score", () =>
|
||||
{
|
||||
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
|
||||
var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
|
||||
double totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
|
||||
return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult;
|
||||
});
|
||||
|
||||
@@ -201,24 +204,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
|
||||
}
|
||||
|
||||
private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
|
||||
{
|
||||
Frames = scoreReplay
|
||||
.Frames
|
||||
.Cast<OsuReplayFrame>()
|
||||
.Select(replayFrame =>
|
||||
{
|
||||
var adjustedTime = replayFrame.Time * rate;
|
||||
return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray());
|
||||
})
|
||||
.Cast<ReplayFrame>()
|
||||
.ToList()
|
||||
};
|
||||
|
||||
private void addSeekStep(double time)
|
||||
{
|
||||
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
|
||||
|
||||
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
|
||||
}
|
||||
|
||||
@@ -241,7 +229,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
new Spinner
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
EndTime = 6000,
|
||||
StartTime = spinner_start_time,
|
||||
Duration = spinner_duration
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -369,8 +369,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
},
|
||||
});
|
||||
|
||||
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
p.OnLoadComplete += _ =>
|
||||
@@ -399,6 +397,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public TestSlider()
|
||||
{
|
||||
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
|
||||
|
||||
DefaultsApplied += _ =>
|
||||
{
|
||||
HeadCircle.HitWindows = new TestHitWindows();
|
||||
|
||||
@@ -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.11.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
|
||||
@@ -11,6 +11,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
{
|
||||
@@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset,
|
||||
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
||||
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / beatmap.ControlPointInfo.DifficultyPointAt(original.StartTime).SpeedMultiplier : 1
|
||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1
|
||||
}.Yield();
|
||||
|
||||
case IHasDuration endTimeData:
|
||||
|
||||
@@ -10,9 +10,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
public double AimStrain { get; set; }
|
||||
public double SpeedStrain { get; set; }
|
||||
public double FlashlightRating { get; set; }
|
||||
public double SliderFactor { get; set; }
|
||||
public double ApproachRate { get; set; }
|
||||
public double OverallDifficulty { get; set; }
|
||||
public double DrainRate { get; set; }
|
||||
public int HitCircleCount { get; set; }
|
||||
public int SliderCount { get; set; }
|
||||
public int SpinnerCount { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return new OsuDifficultyAttributes { Mods = mods, Skills = skills };
|
||||
|
||||
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
|
||||
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
|
||||
double flashlightRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
|
||||
double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
|
||||
double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
|
||||
double flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier;
|
||||
|
||||
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
|
||||
|
||||
if (mods.Any(h => h is OsuModRelax))
|
||||
speedRating = 0.0;
|
||||
|
||||
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
|
||||
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
|
||||
@@ -53,13 +59,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
|
||||
|
||||
double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
double drainRate = beatmap.Difficulty.DrainRate;
|
||||
|
||||
int maxCombo = beatmap.HitObjects.Count;
|
||||
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
|
||||
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
|
||||
|
||||
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
||||
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
||||
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
|
||||
|
||||
return new OsuDifficultyAttributes
|
||||
@@ -69,10 +77,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
AimStrain = aimRating,
|
||||
SpeedStrain = speedRating,
|
||||
FlashlightRating = flashlightRating,
|
||||
SliderFactor = sliderFactor,
|
||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
||||
DrainRate = drainRate,
|
||||
MaxCombo = maxCombo,
|
||||
HitCircleCount = hitCirclesCount,
|
||||
SliderCount = sliderCount,
|
||||
SpinnerCount = spinnerCount,
|
||||
Skills = skills
|
||||
};
|
||||
@@ -95,14 +106,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
{
|
||||
HitWindows hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
// Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
||||
hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate;
|
||||
hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
|
||||
return new Skill[]
|
||||
{
|
||||
new Aim(mods),
|
||||
new Aim(mods, true),
|
||||
new Aim(mods, false),
|
||||
new Speed(mods, hitWindowGreat),
|
||||
new Flashlight(mods)
|
||||
};
|
||||
|
||||
@@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
private int countMeh;
|
||||
private int countMiss;
|
||||
|
||||
private int effectiveMissCount;
|
||||
|
||||
public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
|
||||
: base(ruleset, attributes, score)
|
||||
{
|
||||
@@ -39,16 +41,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
effectiveMissCount = calculateEffectiveMissCount();
|
||||
|
||||
// Custom multipliers for NoFail and SpunOut.
|
||||
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||
|
||||
// Custom multipliers for NoFail and SpunOut.
|
||||
if (mods.Any(m => m is OsuModNoFail))
|
||||
multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss);
|
||||
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
|
||||
|
||||
if (mods.Any(m => m is OsuModSpunOut))
|
||||
multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85);
|
||||
|
||||
if (mods.Any(h => h is OsuModRelax))
|
||||
{
|
||||
// As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
|
||||
effectiveMissCount = Math.Min(effectiveMissCount + countOk + countMeh, totalHits);
|
||||
|
||||
multiplier *= 0.6;
|
||||
}
|
||||
|
||||
double aimValue = computeAimValue();
|
||||
double speedValue = computeSpeedValue();
|
||||
double accuracyValue = computeAccuracyValue();
|
||||
@@ -91,8 +102,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
aimValue *= lengthBonus;
|
||||
|
||||
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
||||
if (countMiss > 0)
|
||||
aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss);
|
||||
if (effectiveMissCount > 0)
|
||||
aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), effectiveMissCount);
|
||||
|
||||
// Combo scaling.
|
||||
if (Attributes.MaxCombo > 0)
|
||||
@@ -100,22 +111,31 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double approachRateFactor = 0.0;
|
||||
if (Attributes.ApproachRate > 10.33)
|
||||
approachRateFactor = Attributes.ApproachRate - 10.33;
|
||||
approachRateFactor = 0.3 * (Attributes.ApproachRate - 10.33);
|
||||
else if (Attributes.ApproachRate < 8.0)
|
||||
approachRateFactor = 0.025 * (8.0 - Attributes.ApproachRate);
|
||||
approachRateFactor = 0.1 * (8.0 - Attributes.ApproachRate);
|
||||
|
||||
double approachRateTotalHitsFactor = 1.0 / (1.0 + Math.Exp(-(0.007 * (totalHits - 400))));
|
||||
aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
|
||||
|
||||
double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
|
||||
|
||||
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
||||
if (mods.Any(h => h is OsuModHidden))
|
||||
if (mods.Any(m => m is OsuModBlinds))
|
||||
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate);
|
||||
else if (mods.Any(h => h is OsuModHidden))
|
||||
{
|
||||
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
||||
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
|
||||
}
|
||||
|
||||
aimValue *= approachRateBonus;
|
||||
// We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator.
|
||||
double estimateDifficultSliders = Attributes.SliderCount * 0.15;
|
||||
|
||||
// Scale the aim value with accuracy _slightly_.
|
||||
aimValue *= 0.5 + accuracy / 2.0;
|
||||
if (Attributes.SliderCount > 0)
|
||||
{
|
||||
double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, Attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
|
||||
double sliderNerfFactor = (1 - Attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + Attributes.SliderFactor;
|
||||
aimValue *= sliderNerfFactor;
|
||||
}
|
||||
|
||||
aimValue *= accuracy;
|
||||
// It is important to also consider accuracy difficulty when doing that.
|
||||
aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
|
||||
|
||||
@@ -132,8 +152,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
speedValue *= lengthBonus;
|
||||
|
||||
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
||||
if (countMiss > 0)
|
||||
speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));
|
||||
if (effectiveMissCount > 0)
|
||||
speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
|
||||
|
||||
// Combo scaling.
|
||||
if (Attributes.MaxCombo > 0)
|
||||
@@ -141,17 +161,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double approachRateFactor = 0.0;
|
||||
if (Attributes.ApproachRate > 10.33)
|
||||
approachRateFactor = Attributes.ApproachRate - 10.33;
|
||||
approachRateFactor = 0.3 * (Attributes.ApproachRate - 10.33);
|
||||
|
||||
double approachRateTotalHitsFactor = 1.0 / (1.0 + Math.Exp(-(0.007 * (totalHits - 400))));
|
||||
speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
|
||||
|
||||
speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
|
||||
|
||||
if (mods.Any(m => m is OsuModHidden))
|
||||
if (mods.Any(m => m is OsuModBlinds))
|
||||
{
|
||||
// Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
|
||||
speedValue *= 1.12;
|
||||
}
|
||||
else if (mods.Any(m => m is OsuModHidden))
|
||||
{
|
||||
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
||||
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
|
||||
}
|
||||
|
||||
// Scale the speed value with accuracy and OD.
|
||||
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2);
|
||||
|
||||
// Scale the speed value with # of 50s to punish doubletapping.
|
||||
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
||||
|
||||
@@ -160,6 +187,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
private double computeAccuracyValue()
|
||||
{
|
||||
if (mods.Any(h => h is OsuModRelax))
|
||||
return 0.0;
|
||||
|
||||
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
|
||||
double betterAccuracyPercentage;
|
||||
int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;
|
||||
@@ -180,8 +210,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
|
||||
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
|
||||
|
||||
if (mods.Any(m => m is OsuModHidden))
|
||||
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
|
||||
if (mods.Any(m => m is OsuModBlinds))
|
||||
accuracyValue *= 1.14;
|
||||
else if (mods.Any(m => m is OsuModHidden))
|
||||
accuracyValue *= 1.08;
|
||||
|
||||
if (mods.Any(m => m is OsuModFlashlight))
|
||||
accuracyValue *= 1.02;
|
||||
|
||||
@@ -205,8 +239,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
flashlightValue *= 1.3;
|
||||
|
||||
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
||||
if (countMiss > 0)
|
||||
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));
|
||||
if (effectiveMissCount > 0)
|
||||
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
|
||||
|
||||
// Combo scaling.
|
||||
if (Attributes.MaxCombo > 0)
|
||||
@@ -224,6 +258,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return flashlightValue;
|
||||
}
|
||||
|
||||
private int calculateEffectiveMissCount()
|
||||
{
|
||||
// Guess the number of misses + slider breaks from combo
|
||||
double comboBasedMissCount = 0.0;
|
||||
|
||||
if (Attributes.SliderCount > 0)
|
||||
{
|
||||
double fullComboThreshold = Attributes.MaxCombo - 0.1 * Attributes.SliderCount;
|
||||
if (scoreMaxCombo < fullComboThreshold)
|
||||
comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
||||
}
|
||||
|
||||
// Clamp misscount since it's derived from combo and can be higher than total hits and that breaks some calculations
|
||||
comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits);
|
||||
|
||||
return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount));
|
||||
}
|
||||
|
||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user