mirror of
https://github.com/ppy/osu.git
synced 2026-05-14 04:22:34 +08:00
Compare commits
1261 Commits
@@ -21,7 +21,7 @@
|
||||
]
|
||||
},
|
||||
"ppy.localisationanalyser.tools": {
|
||||
"version": "2024.802.0",
|
||||
"version": "2025.1208.0",
|
||||
"commands": [
|
||||
"localisation"
|
||||
]
|
||||
|
||||
@@ -38,8 +38,12 @@ jobs:
|
||||
run: ./UseLocalOsu.sh
|
||||
working-directory: ./osu-tools
|
||||
|
||||
- name: Build tools
|
||||
run: dotnet build PerformanceCalculator --nologo --verbosity quiet
|
||||
working-directory: ./osu-tools
|
||||
|
||||
- name: Regenerate mod definitions
|
||||
run: dotnet run --project PerformanceCalculator -- mods > ../osu-web/database/mods.json
|
||||
run: dotnet run --project PerformanceCalculator --no-build -- mods > ../osu-web/database/mods.json
|
||||
working-directory: ./osu-tools
|
||||
|
||||
- name: Create pull request with changes
|
||||
|
||||
+4
-3
@@ -55,9 +55,7 @@ When in doubt, it's probably best to start with a discussion first. We will esca
|
||||
|
||||
While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change.
|
||||
|
||||
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good first issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
|
||||
|
||||
In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
|
||||
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
|
||||
|
||||
If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! Figma master library](https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Master-Library).
|
||||
|
||||
@@ -73,6 +71,9 @@ Aside from the above, below is a brief checklist of things to watch out when you
|
||||
After you're done with your changes and you wish to open the PR, please observe the following recommendations:
|
||||
|
||||
- Please submit the pull request from a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary.
|
||||
- Please pick the following target branch for your pull request:
|
||||
- `pp-dev`, if the change impacts star rating or performance points calculations for any of the rulesets,
|
||||
- `master`, otherwise.
|
||||
- Please avoid pushing untested or incomplete code.
|
||||
- Please do not force-push or rebase unless we ask you to.
|
||||
- Please do not merge `master` continually if there are no conflicts to resolve. We will do this for you when the change is ready for merge.
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="NUnit" Version="4.5.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
|
||||
|
||||
+3
-3
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.EmptyFreeform
|
||||
{
|
||||
}
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
||||
{
|
||||
return new DifficultyAttributes(mods, 0);
|
||||
}
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>();
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="NUnit" Version="4.5.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||
|
||||
+3
-3
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.Pippidon
|
||||
{
|
||||
}
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
||||
{
|
||||
return new DifficultyAttributes(mods, 0);
|
||||
}
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>();
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="NUnit" Version="4.5.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
||||
|
||||
+3
-3
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.EmptyScrolling
|
||||
{
|
||||
}
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
||||
{
|
||||
return new DifficultyAttributes(mods, 0);
|
||||
}
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>();
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="NUnit" Version="4.5.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||
|
||||
+3
-3
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.Pippidon
|
||||
{
|
||||
}
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
||||
{
|
||||
return new DifficultyAttributes(mods, 0);
|
||||
}
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>();
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1008.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.310.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -5,16 +5,13 @@
|
||||
android:supportsRtl="true"
|
||||
android:label="osu!"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher" />
|
||||
<!-- for editor usage -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<!--
|
||||
READ_MEDIA_* permissions are available only on API 33 or greater. Devices with older android versions
|
||||
don't understand the new permissions, so request the old READ_EXTERNAL_STORAGE permission to get storage access.
|
||||
Since the old permission has no effect on >= API 33, don't request it.
|
||||
|
||||
Care needs to be taken to ensure runtime permission checks target the correct permission for the API level.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
|
||||
android:roundIcon="@mipmap/ic_launcher">
|
||||
<provider android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="sh.ppy.osulazer.fileprovider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/filepaths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<!-- https://developer.android.com/reference/androidx/core/content/FileProvider -->
|
||||
<external-files-path path="logs" name="logs" />
|
||||
<external-files-path path="exports" name="exports" />
|
||||
</paths>
|
||||
@@ -189,7 +189,7 @@ namespace osu.Desktop
|
||||
}
|
||||
|
||||
// user party
|
||||
if (!hideIdentifiableInformation && multiplayerClient.Room != null)
|
||||
if (!hideIdentifiableInformation && multiplayerClient.Room != null && multiplayerClient.Room.Settings.MatchType != MatchType.Matchmaking)
|
||||
{
|
||||
MultiplayerRoom room = multiplayerClient.Room;
|
||||
|
||||
|
||||
@@ -146,9 +146,13 @@ namespace osu.Desktop
|
||||
{
|
||||
base.SetHost(host);
|
||||
|
||||
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
||||
if (iconStream != null)
|
||||
host.Window.SetIconFromStream(iconStream);
|
||||
// Apple operating systems use a better icon provided via external assets.
|
||||
if (!RuntimeInfo.IsApple)
|
||||
{
|
||||
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
||||
if (iconStream != null)
|
||||
host.Window.SetIconFromStream(iconStream);
|
||||
}
|
||||
|
||||
host.Window.Title = Name;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
|
||||
@@ -32,7 +33,7 @@ namespace osu.Desktop.Security
|
||||
{
|
||||
public ElevatedPrivilegesNotification()
|
||||
{
|
||||
Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.";
|
||||
Text = NotificationsStrings.ElevatedPrivileges(RuntimeInfo.IsUnix ? "root" : "Administrator");
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="System.IO.Packaging" Version="9.0.2" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="10.0.5" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.6.1.70" />
|
||||
<PackageReference Include="Velopack" Version="0.0.1298" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
|
||||
@@ -5,7 +5,7 @@ using BenchmarkDotNet.Attributes;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
using osu.Game.Tests.NonVisual.Filtering;
|
||||
|
||||
namespace osu.Game.Benchmarks
|
||||
{
|
||||
@@ -42,7 +42,7 @@ namespace osu.Game.Benchmarks
|
||||
Status = BeatmapOnlineStatus.Loved
|
||||
};
|
||||
|
||||
private CarouselBeatmap carouselBeatmap = null!;
|
||||
private FilterMatchingTest.CarouselBeatmap carouselBeatmap = null!;
|
||||
private FilterCriteria criteria1 = null!;
|
||||
private FilterCriteria criteria2 = null!;
|
||||
private FilterCriteria criteria3 = null!;
|
||||
@@ -55,7 +55,7 @@ namespace osu.Game.Benchmarks
|
||||
var beatmap = getExampleBeatmap();
|
||||
beatmap.OnlineID = 20201010;
|
||||
beatmap.BeatmapSet = new BeatmapSetInfo { OnlineID = 1535 };
|
||||
carouselBeatmap = new CarouselBeatmap(beatmap);
|
||||
carouselBeatmap = new FilterMatchingTest.CarouselBeatmap(beatmap);
|
||||
criteria1 = new FilterCriteria();
|
||||
criteria2 = new FilterCriteria
|
||||
{
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||
<PackageReference Include="nunit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
|
||||
<PackageReference Include="nunit" Version="4.5.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -35,11 +35,9 @@
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Game.Rulesets.Catch.Skinning;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Legacy;
|
||||
@@ -21,9 +22,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
var skinSource = new SkinProvidingContainer(rawSkin);
|
||||
var skin = new CatchLegacySkinTransformer(skinSource);
|
||||
|
||||
Assert.AreEqual(new Color4(232, 185, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value);
|
||||
Assert.AreEqual(new Color4(232, 74, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value);
|
||||
Assert.AreEqual(new Color4(0, 255, 255, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value);
|
||||
ClassicAssert.AreEqual(new Color4(232, 185, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value);
|
||||
ClassicAssert.AreEqual(new Color4(232, 74, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value);
|
||||
ClassicAssert.AreEqual(new Color4(0, 255, 255, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value);
|
||||
}
|
||||
|
||||
private class TestLegacySkin : LegacySkin
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
|
||||
<PackageReference Include="NUnit" Version="4.5.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
||||
@@ -176,15 +176,20 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch };
|
||||
|
||||
protected override IEnumerable<HitResult> GetValidHitResults()
|
||||
public override IEnumerable<HitResult> GetValidHitResults()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
HitResult.Great,
|
||||
HitResult.Miss,
|
||||
|
||||
HitResult.LargeTickHit,
|
||||
HitResult.LargeTickMiss,
|
||||
HitResult.SmallTickHit,
|
||||
HitResult.SmallTickMiss,
|
||||
HitResult.LargeBonus,
|
||||
HitResult.IgnoreHit,
|
||||
HitResult.IgnoreMiss,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -300,7 +305,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
Description = "Affects how early fruits fade in on the screen.",
|
||||
AdditionalMetrics =
|
||||
[
|
||||
new RulesetBeatmapAttribute.AdditionalMetric("Fade-in time", LocalisableString.Interpolate($@"{IBeatmapDifficultyInfo.DifficultyRange(effectiveDifficulty.ApproachRate, CatchHitObject.PREEMPT_RANGE):#,0.##} ms"))
|
||||
new RulesetBeatmapAttribute.AdditionalMetric("Fade-in time", LocalisableString.Interpolate($@"{IBeatmapDifficultyInfo.DifficultyRangeInt(effectiveDifficulty.ApproachRate, CatchHitObject.PREEMPT_RANGE):#,0.##} ms"))
|
||||
]
|
||||
};
|
||||
yield return new RulesetBeatmapAttribute(SongSelectStrings.HPDrain, @"HP", originalDifficulty.DrainRate, effectiveDifficulty.DrainRate, 10)
|
||||
|
||||
@@ -15,6 +15,7 @@ using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
@@ -22,16 +23,14 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
private const double difficulty_multiplier = 4.59;
|
||||
|
||||
private float halfCatcherWidth;
|
||||
|
||||
public override int Version => 20250306;
|
||||
public override int Version => 20251020;
|
||||
|
||||
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
{
|
||||
}
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
||||
{
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
return new CatchDifficultyAttributes { Mods = mods };
|
||||
@@ -46,12 +45,19 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
return attributes;
|
||||
}
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods)
|
||||
{
|
||||
CatchHitObject? lastObject = null;
|
||||
|
||||
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
||||
|
||||
double clockRate = ModUtils.CalculateRateWithMods(mods);
|
||||
|
||||
float 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.Difficulty.CircleSize - 5.5f) * 0.0625f);
|
||||
|
||||
// In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream.
|
||||
foreach (var hitObject in CatchBeatmap.GetPalpableObjects(beatmap.HitObjects))
|
||||
{
|
||||
@@ -68,16 +74,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
return objects;
|
||||
}
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
|
||||
{
|
||||
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.Difficulty.CircleSize - 5.5f) * 0.0625f);
|
||||
|
||||
return new Skill[]
|
||||
{
|
||||
new Movement(mods, halfCatcherWidth, clockRate),
|
||||
new Movement(mods),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,16 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Evaluators
|
||||
{
|
||||
private const double direction_change_bonus = 21.0;
|
||||
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, double catcherSpeedMultiplier)
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||
{
|
||||
var catchCurrent = (CatchDifficultyHitObject)current;
|
||||
var catchLast = (CatchDifficultyHitObject)current.Previous(0);
|
||||
var catchLastLast = (CatchDifficultyHitObject)current.Previous(1);
|
||||
|
||||
// In catch, clockrate adjustments do not only affect the timings of hitobjects,
|
||||
// but also the speed of the player's catcher, which has an impact on difficulty
|
||||
double catcherSpeedMultiplier = current.ClockRate;
|
||||
|
||||
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
|
||||
|
||||
double distanceAddition = (Math.Pow(Math.Abs(catchCurrent.DistanceMoved), 1.3) / 510);
|
||||
@@ -40,6 +44,30 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Evaluators
|
||||
/ (CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 6) / sqrtStrain;
|
||||
}
|
||||
|
||||
// Linear spacing nerf.
|
||||
double linearSpacingCount = 0;
|
||||
|
||||
for (int i = 0; i < Math.Min(current.Index, 10); i++)
|
||||
{
|
||||
var catchPrevObj = (CatchDifficultyHitObject)catchCurrent.Previous(i);
|
||||
|
||||
// Only same direction movements matter as they do not take any additional inputs.
|
||||
if (Math.Sign(catchCurrent.DistanceMoved) != Math.Sign(catchPrevObj.DistanceMoved) || catchCurrent.DistanceMoved == 0 || catchPrevObj.DistanceMoved == 0)
|
||||
break;
|
||||
|
||||
double currentSpacing = Math.Abs(catchCurrent.DistanceMoved / catchCurrent.StrainTime);
|
||||
double prevSpacing = Math.Abs(catchPrevObj.DistanceMoved / catchPrevObj.StrainTime);
|
||||
|
||||
double relativeDifference = Math.Abs(currentSpacing / prevSpacing - 1);
|
||||
|
||||
if (relativeDifference > 0.05)
|
||||
break;
|
||||
|
||||
linearSpacingCount++;
|
||||
}
|
||||
|
||||
distanceAddition *= Math.Pow(0.7, linearSpacingCount);
|
||||
|
||||
// Bonus for edge dashes.
|
||||
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
|
||||
{
|
||||
|
||||
@@ -17,28 +17,14 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
|
||||
protected override int SectionLength => 750;
|
||||
|
||||
protected readonly float HalfCatcherWidth;
|
||||
|
||||
/// <summary>
|
||||
/// The speed multiplier applied to the player's catcher.
|
||||
/// </summary>
|
||||
private readonly double catcherSpeedMultiplier;
|
||||
|
||||
public Movement(Mod[] mods, float halfCatcherWidth, double clockRate)
|
||||
public Movement(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
HalfCatcherWidth = halfCatcherWidth;
|
||||
|
||||
// In catch, clockrate adjustments do not only affect the timings of hitobjects,
|
||||
// but also the speed of the player's catcher, which has an impact on difficulty
|
||||
// TODO: Support variable clockrates caused by mods such as ModTimeRamp
|
||||
// (perhaps by using IApplicableToRate within the CatchDifficultyHitObject constructor to set a catcher speed for each object before processing)
|
||||
catcherSpeedMultiplier = clockRate;
|
||||
}
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
return MovementEvaluator.EvaluateDifficultyOf(current, catcherSpeedMultiplier);
|
||||
return MovementEvaluator.EvaluateDifficultyOf(current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
private double placementStartTime;
|
||||
private double placementEndTime;
|
||||
|
||||
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
|
||||
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0));
|
||||
|
||||
public BananaShowerPlacementBlueprint()
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
|
||||
private InputManager inputManager = null!;
|
||||
|
||||
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
|
||||
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0));
|
||||
|
||||
public JuiceStreamPlacementBlueprint()
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -224,7 +225,8 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
#region Clipboard handling
|
||||
|
||||
public override string ConvertSelectionToString()
|
||||
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<CatchHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
|
||||
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<CatchHitObject>().OrderBy(h => h.StartTime)
|
||||
.Select(h => (h.IndexInCurrentCombo + 1).ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
// 1,2,3,4 ...
|
||||
private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled);
|
||||
|
||||
@@ -51,7 +51,8 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
|
||||
return string.Empty;
|
||||
|
||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
string format(string acronym, DifficultyBindable bindable)
|
||||
=> $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Catch.Judgements;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@@ -53,13 +54,25 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
public override IEnumerable<string> LookupNames => lookup_names;
|
||||
|
||||
public BananaHitSampleInfo(int volume = 100)
|
||||
: base(string.Empty, volume: volume)
|
||||
public BananaHitSampleInfo()
|
||||
: this(string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default)
|
||||
=> new BananaHitSampleInfo(newVolume.GetOr(Volume));
|
||||
public BananaHitSampleInfo(HitSampleInfo info)
|
||||
: this(info.Name, info.Bank, info.Suffix, info.Volume, info.EditorAutoBank, info.UseBeatmapSamples)
|
||||
{
|
||||
}
|
||||
|
||||
private BananaHitSampleInfo(string name, string bank = SampleControlPoint.DEFAULT_BANK, string? suffix = null, int volume = 100, bool editorAutoBank = true, bool useBeatmapSamples = false)
|
||||
: base(name, bank, suffix, volume, editorAutoBank, useBeatmapSamples)
|
||||
{
|
||||
}
|
||||
|
||||
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default,
|
||||
Optional<bool> newEditorAutoBank = default, Optional<bool> newUseBeatmapSamples = default)
|
||||
=> new BananaHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume),
|
||||
newEditorAutoBank.GetOr(EditorAutoBank), newUseBeatmapSamples.GetOr(UseBeatmapSamples));
|
||||
|
||||
public bool Equals(BananaHitSampleInfo? other)
|
||||
=> other != null;
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
StartTime = time,
|
||||
BananaIndex = count,
|
||||
Samples = new List<HitSampleInfo> { new Banana.BananaHitSampleInfo(CreateHitSampleInfo().Volume) }
|
||||
Samples = new List<HitSampleInfo> { new Banana.BananaHitSampleInfo(CreateHitSampleInfo()) }
|
||||
});
|
||||
|
||||
count++;
|
||||
|
||||
@@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_RANGE);
|
||||
TimePreempt = IBeatmapDifficultyInfo.DifficultyRangeInt(difficulty.ApproachRate, PREEMPT_RANGE);
|
||||
|
||||
Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize);
|
||||
}
|
||||
|
||||
@@ -72,6 +72,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
leaderboard.Origin = Anchor.CentreLeft;
|
||||
leaderboard.X = 10;
|
||||
}
|
||||
|
||||
foreach (var d in container.OfType<ISerialisableDrawable>())
|
||||
d.UsesFixedAnchor = true;
|
||||
})
|
||||
{
|
||||
Children = new Drawable[]
|
||||
|
||||
@@ -35,11 +35,9 @@
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
// 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.Testing;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
using DragArea = osu.Game.Screens.Edit.Compose.Components.Timeline.TimelineHitObjectBlueprint.DragArea;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneHoldNoteTailDrag : EditorTestScene
|
||||
{
|
||||
protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
AddStep("Clear objects", () => EditorBeatmap.Clear());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSimpleTailDragForward()
|
||||
{
|
||||
AddStep("Add hold note", () =>
|
||||
{
|
||||
EditorBeatmap.Add(new HoldNote { StartTime = 2170, Duration = 937.5 });
|
||||
});
|
||||
|
||||
AddStep("Drag tail", () =>
|
||||
{
|
||||
var blueprintDragArea = this.ChildrenOfType<DragArea>().Single();
|
||||
dragForward(blueprintDragArea);
|
||||
});
|
||||
|
||||
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("Duration is higher", () => ((HoldNote)EditorBeatmap.HitObjects.First())!.Duration > 937.5f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSimpleTailDragBackwards()
|
||||
{
|
||||
AddStep("Add hold note", () =>
|
||||
{
|
||||
EditorBeatmap.Add(new HoldNote { StartTime = 2170, Duration = 937.5 });
|
||||
});
|
||||
|
||||
AddStep("Drag tail", () =>
|
||||
{
|
||||
var blueprintDragArea = this.ChildrenOfType<DragArea>().Single();
|
||||
dragBackward(blueprintDragArea);
|
||||
});
|
||||
|
||||
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("Duration is lower", () => ((HoldNote)EditorBeatmap.HitObjects[0]).Duration < 937.5f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSamePositionButNotSelectedDragForward()
|
||||
{
|
||||
AddStep("Add hold notes", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange([
|
||||
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
|
||||
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 1 }
|
||||
]);
|
||||
});
|
||||
|
||||
AddStep("Drag tail", () =>
|
||||
{
|
||||
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
|
||||
dragForward(blueprintDragArea);
|
||||
});
|
||||
|
||||
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("Duration is higher, other is unchanged", () =>
|
||||
((HoldNote)EditorBeatmap.HitObjects[0]).Duration > 937.5f &&
|
||||
((HoldNote)EditorBeatmap.HitObjects[^1]).Duration == 937.5f
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSamePositionButNotSelectedDragBackward()
|
||||
{
|
||||
AddStep("Add hold notes", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange([
|
||||
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
|
||||
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 1 }
|
||||
]);
|
||||
});
|
||||
|
||||
AddStep("Drag tail", () =>
|
||||
{
|
||||
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
|
||||
dragBackward(blueprintDragArea);
|
||||
});
|
||||
|
||||
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("Duration is lower, other is unchanged", () =>
|
||||
((HoldNote)EditorBeatmap.HitObjects[0]).Duration < 937.5f &&
|
||||
((HoldNote)EditorBeatmap.HitObjects[^1]).Duration == 937.5f
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSamePositionSelectedDragForward()
|
||||
{
|
||||
AddStep("Add hold notes", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange([
|
||||
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
|
||||
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 1 }
|
||||
]);
|
||||
});
|
||||
|
||||
AddStep("Select all", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects);
|
||||
});
|
||||
|
||||
AddStep("Drag tail", () =>
|
||||
{
|
||||
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
|
||||
dragForward(blueprintDragArea);
|
||||
});
|
||||
|
||||
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("Both durations are higher", () =>
|
||||
((HoldNote)EditorBeatmap.HitObjects[0]).Duration > 937.5f &&
|
||||
((HoldNote)EditorBeatmap.HitObjects[^1]).Duration > 937.5f
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSamePositionSelectedDragBackward()
|
||||
{
|
||||
AddStep("Add hold notes", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange([
|
||||
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
|
||||
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 1 }
|
||||
]);
|
||||
});
|
||||
|
||||
AddStep("Select all", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects);
|
||||
});
|
||||
|
||||
AddStep("Drag tail", () =>
|
||||
{
|
||||
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
|
||||
dragBackward(blueprintDragArea);
|
||||
});
|
||||
|
||||
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("Both durations are lower", () =>
|
||||
((HoldNote)EditorBeatmap.HitObjects[0]).Duration < 937.5f &&
|
||||
((HoldNote)EditorBeatmap.HitObjects[^1]).Duration < 937.5f
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectedButDifferentPositions()
|
||||
{
|
||||
AddStep("Add hold notes", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange([
|
||||
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
|
||||
new HoldNote { StartTime = 2404, Duration = 937.5, Column = 1 }
|
||||
]);
|
||||
});
|
||||
|
||||
AddStep("Select all", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects);
|
||||
});
|
||||
|
||||
AddStep("Drag tail", () =>
|
||||
{
|
||||
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
|
||||
dragBackward(blueprintDragArea);
|
||||
});
|
||||
|
||||
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("Duration is unchanged, other is lower", () =>
|
||||
((HoldNote)EditorBeatmap.HitObjects[0]).Duration == 937.5f &&
|
||||
((HoldNote)EditorBeatmap.HitObjects[^1]).Duration < 937.5f
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectedSameStartTimeDifferentDurations()
|
||||
{
|
||||
AddStep("Add hold notes", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange([
|
||||
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
|
||||
new HoldNote { StartTime = 2170, Duration = 1171.8, Column = 1 }
|
||||
]);
|
||||
});
|
||||
|
||||
AddStep("Select all", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects);
|
||||
});
|
||||
|
||||
AddStep("Drag until both match", () =>
|
||||
{
|
||||
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
|
||||
InputManager.MoveMouseTo(blueprintDragArea);
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
InputManager.MoveMouseTo(new Vector2(1000, 110));
|
||||
});
|
||||
|
||||
AddStep("Continue the drag", () =>
|
||||
{
|
||||
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
|
||||
dragBackward(blueprintDragArea);
|
||||
});
|
||||
|
||||
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("Duration is unchanged, other is lower", () =>
|
||||
((HoldNote)EditorBeatmap.HitObjects[0]).Duration == 937.5f &&
|
||||
((HoldNote)EditorBeatmap.HitObjects[^1]).Duration < 937.5f
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectedSameDurationDifferentStartTimes()
|
||||
{
|
||||
AddStep("Add hold notes", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange([
|
||||
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
|
||||
new HoldNote { StartTime = 2638.7, Duration = 937.5, Column = 1 }
|
||||
]);
|
||||
});
|
||||
|
||||
AddStep("Select all", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects);
|
||||
});
|
||||
|
||||
AddStep("Drag tail", () =>
|
||||
{
|
||||
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
|
||||
dragBackward(blueprintDragArea);
|
||||
});
|
||||
|
||||
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("Duration is unchanged, other is lower", () =>
|
||||
((HoldNote)EditorBeatmap.HitObjects[0]).Duration == 937.5f &&
|
||||
((HoldNote)EditorBeatmap.HitObjects[^1]).Duration < 937.5f
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDragNoteOutsideOfSelection()
|
||||
{
|
||||
AddStep("Add hold notes", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange([
|
||||
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
|
||||
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 1 }
|
||||
]);
|
||||
});
|
||||
|
||||
AddStep("Select the back stack slider", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.Last());
|
||||
});
|
||||
|
||||
AddStep("Drag tail", () =>
|
||||
{
|
||||
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
|
||||
dragBackward(blueprintDragArea);
|
||||
});
|
||||
|
||||
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("Duration is lower, other is unchanged", () =>
|
||||
((HoldNote)EditorBeatmap.HitObjects[0]).Duration < 937.5f &&
|
||||
((HoldNote)EditorBeatmap.HitObjects[^1]).Duration == 937.5f
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDragHoldNoteWithNotes()
|
||||
{
|
||||
AddStep("Add notes", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange([
|
||||
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
|
||||
new Note { StartTime = 2170, Column = 1 },
|
||||
new Note { StartTime = 3107.5, Column = 2 },
|
||||
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 3 }
|
||||
]);
|
||||
});
|
||||
|
||||
AddStep("Select all", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects);
|
||||
});
|
||||
|
||||
AddStep("Drag tail", () =>
|
||||
{
|
||||
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
|
||||
dragBackward(blueprintDragArea);
|
||||
});
|
||||
|
||||
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("Both durations are lower", () =>
|
||||
{
|
||||
var holdNotes = EditorBeatmap.HitObjects.OfType<HoldNote>();
|
||||
return holdNotes.First().Duration < 937.5f && holdNotes.Last().Duration < 937.5f;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void dragForward(DragArea dragArea)
|
||||
{
|
||||
InputManager.MoveMouseTo(dragArea);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
InputManager.MoveMouseTo(new Vector2(1100, 110));
|
||||
}
|
||||
|
||||
private void dragBackward(DragArea dragArea)
|
||||
{
|
||||
InputManager.MoveMouseTo(dragArea);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
InputManager.MoveMouseTo(new Vector2(700, 110));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@@ -16,6 +17,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
@@ -36,21 +38,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
[Test]
|
||||
public void TestPlaceBeforeCurrentTimeDownwards()
|
||||
{
|
||||
AddStep("seek to 200", () => HitObjectContainer.Dependencies.Get<EditorClock>().Seek(200));
|
||||
AddStep("move mouse before current time", () =>
|
||||
{
|
||||
var column = this.ChildrenOfType<Column>().Single();
|
||||
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(-100));
|
||||
});
|
||||
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("note start time < 0", () => getNote().StartTime < 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceAfterCurrentTimeDownwards()
|
||||
{
|
||||
AddStep("move mouse after current time", () =>
|
||||
{
|
||||
var column = this.ChildrenOfType<Column>().Single();
|
||||
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(100));
|
||||
@@ -58,7 +47,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("note start time > 0", () => getNote().StartTime > 0);
|
||||
AddAssert("note start time < 200", () => getNote().StartTime < 200);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceAfterCurrentTimeDownwards()
|
||||
{
|
||||
AddStep("seek to 200", () => HitObjectContainer.Dependencies.Get<EditorClock>().Seek(200));
|
||||
AddStep("move mouse after current time", () =>
|
||||
{
|
||||
var column = this.ChildrenOfType<Column>().Single();
|
||||
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(300));
|
||||
});
|
||||
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("note start time > 200", () => getNote().StartTime > 200);
|
||||
}
|
||||
|
||||
private Note getNote() => this.ChildrenOfType<DrawableNote>().FirstOrDefault()?.HitObject;
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
AddStep("change seek setting to true", () => config.SetValue(OsuSetting.EditorAutoSeekOnPlacement, true));
|
||||
placeObject();
|
||||
AddUntilStep("wait for seek to complete", () => !EditorClock.IsSeeking);
|
||||
AddAssert("seeked forward to object", () => EditorClock.CurrentTime, () => Is.GreaterThan(initialTime));
|
||||
AddAssert("seeked forward to object", () => EditorClock.CurrentTime, () => Is.GreaterThan(initialTime!));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -18,15 +18,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
public void TestNormalSelection()
|
||||
{
|
||||
addStepClickLink("00:05:920 (5920|3,6623|3,6857|2,7326|1)");
|
||||
AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, new List<(int, int)>
|
||||
{ (5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1) }
|
||||
));
|
||||
AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, [(5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1)]));
|
||||
|
||||
addReset();
|
||||
addStepClickLink("00:42:716 (42716|3,43420|2,44123|0,44357|1,45295|1)");
|
||||
AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, new List<(int, int)>
|
||||
{ (42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1) }
|
||||
));
|
||||
AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, [(42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1)]));
|
||||
|
||||
addReset();
|
||||
AddStep("add notes to row", () =>
|
||||
@@ -41,15 +37,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
EditorBeatmap.AddRange(new[] { second, third, forth });
|
||||
});
|
||||
addStepClickLink("00:11:545 (11545|0,11545|1,11545|2,11545|3)");
|
||||
AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, new List<(int, int)>
|
||||
{ (11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3) }
|
||||
));
|
||||
AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, [(11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3)]));
|
||||
|
||||
addReset();
|
||||
addStepClickLink("01:36:623 (96623|1,97560|1,97677|1,97795|1,98966|1)");
|
||||
AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, new List<(int, int)>
|
||||
{ (96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1) }
|
||||
));
|
||||
AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, [(96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1)]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRoundingToNearestMillisecondApplied()
|
||||
{
|
||||
AddStep("resnap note to have fractional coordinates",
|
||||
() => EditorBeatmap.HitObjects.OfType<ManiaHitObject>().Single(ho => ho.StartTime == 85_373 && ho.Column == 1).StartTime = 85_373.125);
|
||||
addStepClickLink("01:25:373 (85373|1)");
|
||||
AddAssert("selected note", () => checkSnapAndSelectColumn(85_373.125, [(85_373.125, 1)]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -75,7 +76,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
|
||||
private void addReset() => addStepClickLink("00:00:000", "reset", false);
|
||||
|
||||
private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)>? columnPairs = null)
|
||||
private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(double, int)>? columnPairs = null)
|
||||
{
|
||||
bool checkColumns = columnPairs != null
|
||||
? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2)))
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
[TestCase("mania-samples")]
|
||||
[TestCase("mania-slider")] // e.g. second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407
|
||||
[TestCase("slider-convert-samples")]
|
||||
[TestCase("spinner-convert-samples")]
|
||||
public void Test(string name) => base.Test(name);
|
||||
|
||||
protected override IEnumerable<SampleConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Screens.Select;
|
||||
@@ -18,19 +19,19 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "1");
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
ClassicAssert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
ClassicAssert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
ClassicAssert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
ClassicAssert.True(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
@@ -44,19 +45,19 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "1,3,5,7");
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
ClassicAssert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
ClassicAssert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
ClassicAssert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
ClassicAssert.True(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
@@ -70,19 +71,19 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "1");
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
ClassicAssert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
ClassicAssert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
ClassicAssert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
ClassicAssert.False(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
@@ -96,19 +97,19 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "1,3,5,7");
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
ClassicAssert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
ClassicAssert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
ClassicAssert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
ClassicAssert.False(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
@@ -122,23 +123,23 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4");
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
ClassicAssert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
ClassicAssert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
ClassicAssert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
ClassicAssert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 5 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
ClassicAssert.True(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
@@ -153,23 +154,23 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.Greater, "4");
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "7");
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
ClassicAssert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
ClassicAssert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
ClassicAssert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 5 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
ClassicAssert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 7 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
ClassicAssert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 9 }),
|
||||
new FilterCriteria()));
|
||||
}
|
||||
@@ -179,9 +180,9 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "some text"));
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "4,some text"));
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4,5,6"));
|
||||
ClassicAssert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "some text"));
|
||||
ClassicAssert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "4,some text"));
|
||||
ClassicAssert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4,5,6"));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@@ -199,7 +200,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
TotalObjectCount = 0,
|
||||
EndTimeObjectCount = 0
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo1, filterCriteria));
|
||||
ClassicAssert.True(criteria.Matches(beatmapInfo1, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0");
|
||||
BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
@@ -207,7 +208,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 0
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo2, filterCriteria));
|
||||
ClassicAssert.True(criteria.Matches(beatmapInfo2, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "100");
|
||||
BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
@@ -215,7 +216,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 100
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo3, filterCriteria));
|
||||
ClassicAssert.True(criteria.Matches(beatmapInfo3, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "1");
|
||||
BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
@@ -223,7 +224,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 1
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo4, filterCriteria));
|
||||
ClassicAssert.True(criteria.Matches(beatmapInfo4, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0.1");
|
||||
BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
@@ -231,7 +232,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
TotalObjectCount = 1000,
|
||||
EndTimeObjectCount = 1
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo5, filterCriteria));
|
||||
ClassicAssert.True(criteria.Matches(beatmapInfo5, filterCriteria));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@@ -249,7 +250,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
TotalObjectCount = 0,
|
||||
EndTimeObjectCount = 0
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo1, filterCriteria));
|
||||
ClassicAssert.True(criteria.Matches(beatmapInfo1, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0");
|
||||
BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
@@ -257,7 +258,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 0
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo2, filterCriteria));
|
||||
ClassicAssert.True(criteria.Matches(beatmapInfo2, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "100");
|
||||
BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
@@ -265,7 +266,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 100
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo3, filterCriteria));
|
||||
ClassicAssert.True(criteria.Matches(beatmapInfo3, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1");
|
||||
BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
@@ -273,7 +274,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 1
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo4, filterCriteria));
|
||||
ClassicAssert.True(criteria.Matches(beatmapInfo4, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0.1");
|
||||
BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
@@ -281,7 +282,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
TotalObjectCount = 1000,
|
||||
EndTimeObjectCount = 1
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo5, filterCriteria));
|
||||
ClassicAssert.True(criteria.Matches(beatmapInfo5, filterCriteria));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@@ -299,7 +300,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 50
|
||||
};
|
||||
Assert.False(criteria.Matches(beatmapInfo, filterCriteria));
|
||||
ClassicAssert.False(criteria.Matches(beatmapInfo, filterCriteria));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@@ -307,8 +308,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "some text"));
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1some text"));
|
||||
ClassicAssert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "some text"));
|
||||
ClassicAssert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1some text"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
@@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
var definition = new StageDefinition(columns);
|
||||
var results = getResults(definition);
|
||||
Assert.AreEqual(special, results);
|
||||
ClassicAssert.AreEqual(special, results);
|
||||
}
|
||||
|
||||
private IEnumerable<bool> getResults(StageDefinition definition)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Tests.Visual;
|
||||
@@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
public void TestMapHasNoHoldNotes()
|
||||
{
|
||||
var testBeatmap = createModdedBeatmap();
|
||||
Assert.False(testBeatmap.HitObjects.OfType<HoldNote>().Any());
|
||||
ClassicAssert.False(testBeatmap.HitObjects.OfType<HoldNote>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
Mode: 0
|
||||
|
||||
[TimingPoints]
|
||||
0,300,4,0,2,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,2,0:0:0:0:
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"Mappings": [{
|
||||
"StartTime": 1000.0,
|
||||
"Objects": [{
|
||||
"StartTime": 1000.0,
|
||||
"EndTime": 8000.0,
|
||||
"Column": 0,
|
||||
"PlaySlidingSamples": false,
|
||||
"NodeSamples": [
|
||||
["Gameplay/soft-hitnormal"],
|
||||
["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"]
|
||||
],
|
||||
"Samples": ["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"],
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
Mode: 0
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:5
|
||||
CircleSize:5
|
||||
OverallDifficulty:5
|
||||
ApproachRate:5
|
||||
SliderMultiplier:1.4
|
||||
SliderTickRate:1
|
||||
|
||||
[TimingPoints]
|
||||
0,500,4,2,0,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
256,192,1000,8,4,8000,0:2:0:0:
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
@@ -33,11 +34,11 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||
|
||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
||||
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
|
||||
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
|
||||
ClassicAssert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
||||
ClassicAssert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
||||
ClassicAssert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
||||
ClassicAssert.True(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
|
||||
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -54,11 +55,11 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||
|
||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
||||
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
|
||||
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
|
||||
ClassicAssert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
||||
ClassicAssert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
||||
ClassicAssert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
||||
ClassicAssert.True(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
|
||||
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -74,11 +75,11 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||
|
||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
||||
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
|
||||
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
|
||||
ClassicAssert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
||||
ClassicAssert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
||||
ClassicAssert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
||||
ClassicAssert.True(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
|
||||
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -96,13 +97,13 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||
|
||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
||||
ClassicAssert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
||||
|
||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
||||
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
||||
ClassicAssert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
||||
ClassicAssert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
||||
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
|
||||
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
|
||||
ClassicAssert.True(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
|
||||
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -119,15 +120,15 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||
|
||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 4, "Incorrect number of frames");
|
||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
|
||||
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect first note release time");
|
||||
Assert.AreEqual(2000, generated.Frames[frame_offset + 2].Time, "Incorrect second note hit time");
|
||||
Assert.AreEqual(2000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
|
||||
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has not been pressed");
|
||||
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 3], ManiaAction.Key2), "Key2 has not been released");
|
||||
ClassicAssert.AreEqual(generated.Frames.Count, frame_offset + 4, "Incorrect number of frames");
|
||||
ClassicAssert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
|
||||
ClassicAssert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect first note release time");
|
||||
ClassicAssert.AreEqual(2000, generated.Frames[frame_offset + 2].Time, "Incorrect second note hit time");
|
||||
ClassicAssert.AreEqual(2000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
|
||||
ClassicAssert.True(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
|
||||
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
|
||||
ClassicAssert.True(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has not been pressed");
|
||||
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 3], ManiaAction.Key2), "Key2 has not been released");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -146,16 +147,16 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||
|
||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 4, "Incorrect number of frames");
|
||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
|
||||
Assert.AreEqual(3000, generated.Frames[frame_offset + 2].Time, "Incorrect first note release time");
|
||||
Assert.AreEqual(2000, generated.Frames[frame_offset + 1].Time, "Incorrect second note hit time");
|
||||
Assert.AreEqual(4000, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
|
||||
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key1), "Key1 has not been released");
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has been released");
|
||||
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 3], ManiaAction.Key2), "Key2 has not been released");
|
||||
ClassicAssert.AreEqual(generated.Frames.Count, frame_offset + 4, "Incorrect number of frames");
|
||||
ClassicAssert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
|
||||
ClassicAssert.AreEqual(3000, generated.Frames[frame_offset + 2].Time, "Incorrect first note release time");
|
||||
ClassicAssert.AreEqual(2000, generated.Frames[frame_offset + 1].Time, "Incorrect second note hit time");
|
||||
ClassicAssert.AreEqual(4000, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
|
||||
ClassicAssert.True(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
|
||||
ClassicAssert.True(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
|
||||
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key1), "Key1 has not been released");
|
||||
ClassicAssert.True(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has been released");
|
||||
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 3], ManiaAction.Key2), "Key2 has not been released");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -173,14 +174,14 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||
|
||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 3, "Incorrect number of frames");
|
||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
|
||||
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect second note press time + first note release time");
|
||||
Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 2].Time, "Incorrect second note release time");
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
|
||||
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key2), "Key2 has not been pressed");
|
||||
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has not been released");
|
||||
ClassicAssert.AreEqual(generated.Frames.Count, frame_offset + 3, "Incorrect number of frames");
|
||||
ClassicAssert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
|
||||
ClassicAssert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect second note press time + first note release time");
|
||||
ClassicAssert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 2].Time, "Incorrect second note release time");
|
||||
ClassicAssert.True(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
|
||||
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
|
||||
ClassicAssert.True(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key2), "Key2 has not been pressed");
|
||||
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has not been released");
|
||||
}
|
||||
|
||||
private bool checkContains(ReplayFrame frame, params ManiaAction[] actions) => actions.All(action => ((ManiaReplayFrame)frame).Actions.Contains(action));
|
||||
|
||||
@@ -45,5 +45,19 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
AssertBeatmapLookup(expected_sample);
|
||||
AssertNoLookup(unwanted_sample);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestConvertHitObjectCustomSampleBank()
|
||||
{
|
||||
const string beatmap_sample = "normal-hitwhistle2";
|
||||
const string user_skin_sample = "normal-hitnormal";
|
||||
|
||||
SetupSkins(beatmap_sample, user_skin_sample);
|
||||
|
||||
CreateTestWithBeatmap("convert-beatmap-custom-sample-bank.osu");
|
||||
|
||||
AssertBeatmapLookup(beatmap_sample);
|
||||
AssertUserLookup(user_skin_sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
|
||||
<PackageReference Include="NUnit" Version="4.5.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
||||
@@ -85,7 +85,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
Duration = endTime - HitObject.StartTime,
|
||||
Column = column,
|
||||
Samples = HitObject.Samples,
|
||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
|
||||
NodeSamples =
|
||||
[
|
||||
HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_NORMAL).ToList(),
|
||||
HitObject.Samples
|
||||
]
|
||||
};
|
||||
}
|
||||
else
|
||||
|
||||
@@ -19,6 +19,7 @@ using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
@@ -36,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
|
||||
}
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
||||
{
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
return new ManiaDifficultyAttributes { Mods = mods };
|
||||
@@ -62,11 +63,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
return 1;
|
||||
}
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods)
|
||||
{
|
||||
var sortedObjects = beatmap.HitObjects.ToArray();
|
||||
int totalColumns = ((ManiaBeatmap)beatmap).TotalColumns;
|
||||
|
||||
double clockRate = ModUtils.CalculateRateWithMods(mods);
|
||||
|
||||
LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
|
||||
|
||||
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
||||
@@ -88,7 +91,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
// Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
|
||||
protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input;
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
|
||||
{
|
||||
new Strain(mods, ((ManiaBeatmap)Beatmap).TotalColumns)
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; } = null!;
|
||||
|
||||
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
|
||||
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0));
|
||||
|
||||
public HoldNotePlacementBlueprint()
|
||||
: base(new HoldNote())
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// 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 System.Text.RegularExpressions;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
@@ -54,7 +56,8 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
};
|
||||
|
||||
public override string ConvertSelectionToString()
|
||||
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}"));
|
||||
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime)
|
||||
.Select(h => FormattableString.Invariant($"{Math.Round(h.StartTime)}|{h.Column}")));
|
||||
|
||||
// 123|0,456|1,789|2 ...
|
||||
private static readonly Regex selection_regex = new Regex(@"^\d+\|\d+(,\d+\|\d+)*$", RegexOptions.Compiled);
|
||||
@@ -73,10 +76,10 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
if (split.Length != 2)
|
||||
continue;
|
||||
|
||||
if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column))
|
||||
if (!int.TryParse(split[0], out int time) || !int.TryParse(split[1], out int column))
|
||||
continue;
|
||||
|
||||
ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
|
||||
ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => Precision.AlmostEquals(h.StartTime, time, 0.5) && h.Column == column);
|
||||
|
||||
if (current == null)
|
||||
continue;
|
||||
|
||||
@@ -383,7 +383,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast<int>().OrderDescending().First(v => variant >= v);
|
||||
}
|
||||
|
||||
protected override IEnumerable<HitResult> GetValidHitResults()
|
||||
public override IEnumerable<HitResult> GetValidHitResults()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
@@ -392,9 +392,11 @@ namespace osu.Game.Rulesets.Mania
|
||||
HitResult.Good,
|
||||
HitResult.Ok,
|
||||
HitResult.Meh,
|
||||
HitResult.Miss,
|
||||
|
||||
// HitResult.SmallBonus is used for awarding perfect bonus score but is not included here as
|
||||
// it would be a bit redundant to show this to the user.
|
||||
HitResult.IgnoreHit,
|
||||
HitResult.ComboBreak,
|
||||
HitResult.IgnoreMiss,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
@@ -31,47 +31,45 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SettingsEnumDropdown<ManiaScrollingDirection>
|
||||
new SettingsItemV2(new FormEnumDropdown<ManiaScrollingDirection>
|
||||
{
|
||||
LabelText = RulesetSettingsStrings.ScrollingDirection,
|
||||
Caption = RulesetSettingsStrings.ScrollingDirection,
|
||||
Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
|
||||
},
|
||||
new SettingsSlider<double, ManiaScrollSlider>
|
||||
}),
|
||||
new SettingsItemV2(new FormSliderBar<double>
|
||||
{
|
||||
LabelText = RulesetSettingsStrings.ScrollSpeed,
|
||||
Caption = RulesetSettingsStrings.ScrollSpeed,
|
||||
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollSpeed),
|
||||
KeyboardStep = 1
|
||||
},
|
||||
new SettingsCheckbox
|
||||
KeyboardStep = 1,
|
||||
LabelFormat = v => RulesetSettingsStrings.ScrollSpeedTooltip((int)DrawableManiaRuleset.ComputeScrollTime(v), v),
|
||||
}),
|
||||
new SettingsItemV2(new FormCheckBox
|
||||
{
|
||||
Caption = RulesetSettingsStrings.TimingBasedColouring,
|
||||
Current = config.GetBindable<bool>(ManiaRulesetSetting.TimingBasedNoteColouring),
|
||||
})
|
||||
{
|
||||
Keywords = new[] { "color" },
|
||||
LabelText = RulesetSettingsStrings.TimingBasedColouring,
|
||||
Current = config.GetBindable<bool>(ManiaRulesetSetting.TimingBasedNoteColouring),
|
||||
},
|
||||
};
|
||||
|
||||
Add(new SettingsCheckbox
|
||||
Add(new SettingsItemV2(new FormCheckBox
|
||||
{
|
||||
LabelText = RulesetSettingsStrings.TouchOverlay,
|
||||
Caption = RulesetSettingsStrings.TouchOverlay,
|
||||
Current = config.GetBindable<bool>(ManiaRulesetSetting.TouchOverlay)
|
||||
});
|
||||
}));
|
||||
|
||||
if (RuntimeInfo.IsMobile)
|
||||
{
|
||||
Add(new SettingsEnumDropdown<ManiaMobileLayout>
|
||||
Add(new SettingsItemV2(new FormEnumDropdown<ManiaMobileLayout>
|
||||
{
|
||||
LabelText = RulesetSettingsStrings.MobileLayout,
|
||||
Caption = RulesetSettingsStrings.MobileLayout,
|
||||
Current = config.GetBindable<ManiaMobileLayout>(ManiaRulesetSetting.MobileLayout),
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Items = Enum.GetValues<ManiaMobileLayout>().Where(l => l != ManiaMobileLayout.LandscapeWithOverlay),
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private partial class ManiaScrollSlider : RoundedSliderBar<double>
|
||||
{
|
||||
public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip((int)DrawableManiaRuleset.ComputeScrollTime(Current.Value), Current.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
typeof(ManiaModFadeIn)
|
||||
}).ToArray();
|
||||
|
||||
public override bool Ranked => false;
|
||||
public override bool Ranked => true;
|
||||
|
||||
public override bool ValidForFreestyleAsRequiredMod => false;
|
||||
|
||||
|
||||
@@ -7,5 +7,15 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModDifficultyAdjust : ModDifficultyAdjust
|
||||
{
|
||||
public override DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
// Use larger extended limits for mania to include OD values that occur with EZ or HR enabled
|
||||
ExtendedMaxValue = 15,
|
||||
ExtendedMinValue = -15,
|
||||
ReadCurrentFromDifficulty = diff => diff.OverallDifficulty,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
|
||||
@@ -34,6 +35,8 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
var maniaBeatmap = (ManiaBeatmap)beatmap;
|
||||
|
||||
double mostCommonBeatLengthBefore = beatmap.GetMostCommonBeatLength();
|
||||
|
||||
var newObjects = new List<ManiaHitObject>();
|
||||
|
||||
foreach (var h in beatmap.HitObjects.OfType<HoldNote>())
|
||||
@@ -48,6 +51,17 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
}
|
||||
|
||||
maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType<Note>().Concat(newObjects).OrderBy(h => h.StartTime).ToList();
|
||||
|
||||
double mostCommonBeatLengthAfter = beatmap.GetMostCommonBeatLength();
|
||||
|
||||
// the process of removing hold notes can result in shortening the beatmap's play time,
|
||||
// and therefore, as a side effect, changing the most common BPM, which will change scroll speed.
|
||||
// to compensate for this, apply a multiplier to effect points in order to maintain the beatmap's original intended scroll speed.
|
||||
if (!Precision.AlmostEquals(mostCommonBeatLengthBefore, mostCommonBeatLengthAfter))
|
||||
{
|
||||
foreach (var effectPoint in beatmap.ControlPointInfo.EffectPoints)
|
||||
effectPoint.ScrollSpeed *= mostCommonBeatLengthBefore / mostCommonBeatLengthAfter;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
if (spectatorList != null)
|
||||
spectatorList.Position = new Vector2(36, -66);
|
||||
|
||||
foreach (var d in container.OfType<ISerialisableDrawable>())
|
||||
d.UsesFixedAnchor = true;
|
||||
})
|
||||
{
|
||||
new DrawableGameplayLeaderboard(),
|
||||
|
||||
@@ -64,11 +64,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
private readonly Lazy<bool> hasKeyTexture;
|
||||
|
||||
private readonly ManiaBeatmap beatmap;
|
||||
private readonly bool isBeatmapConverted;
|
||||
|
||||
public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap)
|
||||
: base(skin)
|
||||
{
|
||||
this.beatmap = (ManiaBeatmap)beatmap;
|
||||
isBeatmapConverted = !beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
|
||||
|
||||
isLegacySkin = new Lazy<bool>(() => GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version) != null);
|
||||
hasKeyTexture = new Lazy<bool>(() =>
|
||||
@@ -120,6 +122,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
leaderboard.Origin = Anchor.CentreLeft;
|
||||
leaderboard.X = 10;
|
||||
}
|
||||
|
||||
foreach (var d in container.OfType<ISerialisableDrawable>())
|
||||
d.UsesFixedAnchor = true;
|
||||
})
|
||||
{
|
||||
new LegacyManiaComboCounter(),
|
||||
@@ -196,8 +201,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
|
||||
public override ISample GetSample(ISampleInfo sampleInfo)
|
||||
{
|
||||
// layered hit sounds never play in mania
|
||||
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
|
||||
// layered hit sounds never play in mania-native beatmaps (but do play on converts)
|
||||
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered && !isBeatmapConverted)
|
||||
return new SampleVirtual();
|
||||
|
||||
return base.GetSample(sampleInfo);
|
||||
|
||||
@@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
var hitWindows = new ManiaHitWindows();
|
||||
|
||||
AddInternal(judgementPooler = new JudgementPooler<DrawableManiaJudgement>(Enum.GetValues<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r))));
|
||||
AddInternal(judgementPooler = new JudgementPooler<DrawableManiaJudgement>(Enum.GetValues<HitResult>().Where(hitWindows.IsHitResultAllowed)));
|
||||
|
||||
RegisterPool<BarLine, DrawableBarLine>(50, 200);
|
||||
}
|
||||
|
||||
@@ -35,11 +35,9 @@
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
||||
|
||||
@@ -245,13 +245,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddAssert("grid spacing is distance to slider tail", () =>
|
||||
{
|
||||
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01)
|
||||
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.1)
|
||||
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y);
|
||||
});
|
||||
AddAssert("grid rotation points to slider tail", () =>
|
||||
{
|
||||
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||
return Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01);
|
||||
return Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.1);
|
||||
});
|
||||
|
||||
AddStep("start grid placement", () => InputManager.Key(Key.Number5));
|
||||
@@ -280,9 +280,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddAssert("grid spacing and rotation unchanged", () =>
|
||||
{
|
||||
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
|
||||
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01)
|
||||
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.1)
|
||||
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y)
|
||||
&& Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01);
|
||||
&& Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.1);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@@ -22,7 +23,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
[TestFixture]
|
||||
public partial class TestSceneSliderDrawing : TestSceneOsuEditor
|
||||
{
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
|
||||
{
|
||||
var beatmap = new TestBeatmap(ruleset, false);
|
||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTouchInputPlaceHitCircleDirectly()
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
@@ -30,6 +33,16 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
PathType.LINEAR,
|
||||
new Vector2(100, 0),
|
||||
new Vector2(100, 100)
|
||||
),
|
||||
createPathSegment(
|
||||
PathType.PERFECT_CURVE,
|
||||
new Vector2(100.009f, -50.0009f),
|
||||
new Vector2(200.0089f, -100)
|
||||
),
|
||||
createPathSegment(
|
||||
PathType.PERFECT_CURVE,
|
||||
new Vector2(25, -50),
|
||||
new Vector2(100, 75)
|
||||
)
|
||||
};
|
||||
|
||||
@@ -48,9 +61,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
[TestCase(0, 250)]
|
||||
[TestCase(0, 200)]
|
||||
[TestCase(1, 120)]
|
||||
[TestCase(1, 80)]
|
||||
public void TestSliderReversal(int pathIndex, double length)
|
||||
[TestCase(1, 120, false, false)]
|
||||
[TestCase(1, 80, false, false)]
|
||||
[TestCase(2, 250)]
|
||||
[TestCase(2, 190)]
|
||||
[TestCase(3, 250)]
|
||||
[TestCase(3, 190)]
|
||||
public void TestSliderReversal(int pathIndex, double length, bool assertEqualDistances = true, bool assertSliderReduction = true)
|
||||
{
|
||||
var controlPoints = paths[pathIndex];
|
||||
|
||||
@@ -90,6 +107,215 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
InputManager.ReleaseKey(Key.LControl);
|
||||
});
|
||||
|
||||
if (pathIndex == 2)
|
||||
{
|
||||
AddRepeatStep("Reverse slider again", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LControl);
|
||||
InputManager.Key(Key.G);
|
||||
InputManager.ReleaseKey(Key.LControl);
|
||||
}, 2);
|
||||
}
|
||||
|
||||
if (assertEqualDistances)
|
||||
{
|
||||
AddAssert("Middle control point has the same distance from start to end", () =>
|
||||
{
|
||||
var pathControlPoints = selectedSlider.Path.ControlPoints;
|
||||
float middleToStart = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[0].Position);
|
||||
float middleToEnd = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[^1].Position);
|
||||
|
||||
return Precision.AlmostEquals(middleToStart, middleToEnd, 1f);
|
||||
});
|
||||
}
|
||||
|
||||
AddAssert("Middle control point is not at start or end", () =>
|
||||
Vector2.Distance(selectedSlider.Path.ControlPoints[^2].Position, oldStartPos) > 1 &&
|
||||
Vector2.Distance(selectedSlider.Path.ControlPoints[^2].Position, oldEndPos) > 1
|
||||
);
|
||||
|
||||
AddAssert("Slider has correct length", () =>
|
||||
Precision.AlmostEquals(selectedSlider.Path.Distance, oldDistance));
|
||||
|
||||
AddAssert("Slider has correct start position", () =>
|
||||
Vector2.Distance(selectedSlider.Position, oldEndPos) < 1);
|
||||
|
||||
AddAssert("Slider has correct end position", () =>
|
||||
Vector2.Distance(selectedSlider.EndPosition, oldStartPos) < 1);
|
||||
|
||||
AddAssert("Control points have correct types", () =>
|
||||
{
|
||||
var newControlPointTypes = selectedSlider.Path.ControlPoints.Select(p => p.Type).ToArray();
|
||||
|
||||
return oldControlPointTypes.Take(newControlPointTypes.Length).SequenceEqual(newControlPointTypes);
|
||||
});
|
||||
|
||||
if (assertSliderReduction)
|
||||
{
|
||||
AddStep("Move to marker", () =>
|
||||
{
|
||||
var marker = this.ChildrenOfType<SliderEndDragMarker>().Single();
|
||||
var markerPos = (marker.ScreenSpaceDrawQuad.TopRight + marker.ScreenSpaceDrawQuad.BottomRight) / 2;
|
||||
// sometimes the cursor may miss the marker's hitbox so we
|
||||
// add a little offset here to be sure it lands in a clickable position.
|
||||
var position = new Vector2(markerPos.X + 2f, markerPos.Y);
|
||||
InputManager.MoveMouseTo(position);
|
||||
});
|
||||
AddStep("Click", () => InputManager.PressButton(MouseButton.Left));
|
||||
AddStep("Reduce slider", () =>
|
||||
{
|
||||
var middleControlPoint = this.ChildrenOfType<PathControlPointPiece<Slider>>().ToArray()[^2];
|
||||
InputManager.MoveMouseTo(middleControlPoint);
|
||||
});
|
||||
AddStep("Release click", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddStep("Save half slider info", () =>
|
||||
{
|
||||
oldStartPos = selectedSlider.Position;
|
||||
oldEndPos = selectedSlider.EndPosition;
|
||||
oldDistance = selectedSlider.Path.Distance;
|
||||
});
|
||||
|
||||
AddStep("Reverse slider", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LControl);
|
||||
InputManager.Key(Key.G);
|
||||
InputManager.ReleaseKey(Key.LControl);
|
||||
});
|
||||
|
||||
AddAssert("Middle control point has the same distance from start to end", () =>
|
||||
{
|
||||
var pathControlPoints = selectedSlider.Path.ControlPoints;
|
||||
float middleToStart = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[0].Position);
|
||||
float middleToEnd = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[^1].Position);
|
||||
|
||||
return Precision.AlmostEquals(middleToStart, middleToEnd, 1f);
|
||||
});
|
||||
|
||||
AddAssert("Middle control point is not at start or end", () =>
|
||||
Vector2.Distance(selectedSlider.Path.ControlPoints[^2].Position, oldStartPos) > 1 &&
|
||||
Vector2.Distance(selectedSlider.Path.ControlPoints[^2].Position, oldEndPos) > 1
|
||||
);
|
||||
|
||||
AddAssert("Slider has correct length", () =>
|
||||
Precision.AlmostEquals(selectedSlider.Path.Distance, oldDistance));
|
||||
|
||||
AddAssert("Slider has correct start position", () =>
|
||||
Vector2.Distance(selectedSlider.Position, oldEndPos) < 1);
|
||||
|
||||
AddAssert("Slider has correct end position", () =>
|
||||
Vector2.Distance(selectedSlider.EndPosition, oldStartPos) < 1);
|
||||
|
||||
AddAssert("Control points have correct types", () =>
|
||||
{
|
||||
var newControlPointTypes = selectedSlider.Path.ControlPoints.Select(p => p.Type).ToArray();
|
||||
|
||||
return oldControlPointTypes.Take(newControlPointTypes.Length).SequenceEqual(newControlPointTypes);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSegmentedSliderReversal()
|
||||
{
|
||||
PathControlPoint[] segmentedSliderPath =
|
||||
[
|
||||
new PathControlPoint
|
||||
{
|
||||
Position = new Vector2(0, 0),
|
||||
Type = PathType.PERFECT_CURVE
|
||||
},
|
||||
new PathControlPoint
|
||||
{
|
||||
Position = new Vector2(100, 150),
|
||||
},
|
||||
new PathControlPoint
|
||||
{
|
||||
Position = new Vector2(75, -50),
|
||||
Type = PathType.PERFECT_CURVE
|
||||
},
|
||||
new PathControlPoint
|
||||
{
|
||||
Position = new Vector2(225, -75),
|
||||
},
|
||||
new PathControlPoint
|
||||
{
|
||||
Position = new Vector2(350, 50),
|
||||
Type = PathType.PERFECT_CURVE
|
||||
},
|
||||
new PathControlPoint
|
||||
{
|
||||
Position = new Vector2(500, -75),
|
||||
},
|
||||
new PathControlPoint
|
||||
{
|
||||
Position = new Vector2(350, -120),
|
||||
},
|
||||
];
|
||||
|
||||
Vector2 oldStartPos = default;
|
||||
Vector2 oldEndPos = default;
|
||||
double oldDistance = default;
|
||||
|
||||
var oldControlPointTypes = segmentedSliderPath.Select(p => p.Type);
|
||||
|
||||
AddStep("Add slider", () =>
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
Position = new Vector2(0, 200),
|
||||
Path = new SliderPath(segmentedSliderPath)
|
||||
{
|
||||
ExpectedDistance = { Value = 1314 }
|
||||
}
|
||||
};
|
||||
|
||||
EditorBeatmap.Add(slider);
|
||||
|
||||
oldStartPos = slider.Position;
|
||||
oldEndPos = slider.EndPosition;
|
||||
oldDistance = slider.Path.Distance;
|
||||
});
|
||||
|
||||
AddStep("Select slider", () =>
|
||||
{
|
||||
var slider = (Slider)EditorBeatmap.HitObjects[0];
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
});
|
||||
|
||||
AddRepeatStep("Reverse slider", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LControl);
|
||||
InputManager.Key(Key.G);
|
||||
InputManager.ReleaseKey(Key.LControl);
|
||||
}, 3);
|
||||
|
||||
AddAssert("First arc's control is not at the slider's middle", () =>
|
||||
Vector2.Distance(selectedSlider.Path.ControlPoints[^2].Position, selectedSlider.Path.PositionAt(0.5)) > 1
|
||||
);
|
||||
|
||||
AddAssert("Last arc's control is not at the slider's middle", () =>
|
||||
Vector2.Distance(selectedSlider.Path.ControlPoints[1].Position, selectedSlider.Path.PositionAt(0.5)) > 1
|
||||
);
|
||||
|
||||
AddAssert("First arc centered middle control point", () =>
|
||||
{
|
||||
var pathControlPoints = selectedSlider.Path.ControlPoints;
|
||||
float middleToStart = Vector2.Distance(pathControlPoints[1].Position, pathControlPoints[0].Position);
|
||||
float middleToEnd = Vector2.Distance(pathControlPoints[1].Position, pathControlPoints[2].Position);
|
||||
|
||||
return Precision.AlmostEquals(middleToStart, middleToEnd, 1f);
|
||||
});
|
||||
|
||||
AddAssert("Last arc centered middle control point", () =>
|
||||
{
|
||||
var pathControlPoints = selectedSlider.Path.ControlPoints;
|
||||
float middleToStart = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[^3].Position);
|
||||
float middleToEnd = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[^1].Position);
|
||||
|
||||
return Precision.AlmostEquals(middleToStart, middleToEnd, 1f);
|
||||
});
|
||||
|
||||
AddAssert("Slider has correct length", () =>
|
||||
Precision.AlmostEquals(selectedSlider.Path.Distance, oldDistance));
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
@@ -102,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
Assert.True(Precision.AlmostEquals(sliderPathPerfect.PositionAt(i / 100.0f), sliderPathBezier.PositionAt(i / 100.0f)));
|
||||
ClassicAssert.True(Precision.AlmostEquals(sliderPathPerfect.PositionAt(i / 100.0f), sliderPathBezier.PositionAt(i / 100.0f)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +175,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
double theta = circularArcProperties.ThetaStart + (circularArcProperties.Direction * progress * circularArcProperties.ThetaRange);
|
||||
Vector2 vector = new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)) * circularArcProperties.Radius;
|
||||
|
||||
Assert.True(Precision.AlmostEquals(circularArcProperties.Centre + vector, path.PositionAt(progress), 0.01f),
|
||||
ClassicAssert.True(Precision.AlmostEquals(circularArcProperties.Centre + vector, path.PositionAt(progress), 0.01f),
|
||||
"A perfect circle with points " + string.Join(", ", path.ControlPoints.Select(x => x.Position)) + " and radius" + circularArcProperties.Radius + "from SliderPath does not almost equal a theoretical perfect circle with " + subpoints + " subpoints"
|
||||
+ ": " + (circularArcProperties.Centre + vector) + " - " + path.PositionAt(progress)
|
||||
+ " = " + (circularArcProperties.Centre + vector - path.PositionAt(progress))
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneOsuModEasy : OsuModTestScene
|
||||
{
|
||||
protected override bool AllowFail => true;
|
||||
|
||||
[Test]
|
||||
public void TestMultipleApplication()
|
||||
{
|
||||
bool reapplied = false;
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mods = [new OsuModEasy { Retries = { Value = 1 } }],
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () =>
|
||||
{
|
||||
// do stuff to speed up fails
|
||||
var b = new TestBeatmap(new OsuRuleset().RulesetInfo)
|
||||
{
|
||||
Difficulty = { DrainRate = 10 }
|
||||
};
|
||||
|
||||
foreach (var ho in b.HitObjects)
|
||||
ho.StartTime /= 4;
|
||||
|
||||
return b;
|
||||
},
|
||||
PassCondition = () =>
|
||||
{
|
||||
if (((ModEasyTestPlayer)Player).FailuresSuppressed > 0 && !reapplied)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var mod in Player.GameplayState.Mods.OfType<IApplicableToDifficulty>())
|
||||
mod.ApplyToDifficulty(new BeatmapDifficulty());
|
||||
|
||||
foreach (var mod in Player.GameplayState.Mods.OfType<IApplicableToPlayer>())
|
||||
mod.ApplyToPlayer(Player);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// don't care if this fails. in fact a failure here is probably better than the alternative.
|
||||
}
|
||||
finally
|
||||
{
|
||||
reapplied = true;
|
||||
}
|
||||
}
|
||||
|
||||
return Player.GameplayState.HasFailed && ((ModEasyTestPlayer)Player).FailuresSuppressed <= 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModEasyTestPlayer(CurrentTestData, AllowFail);
|
||||
|
||||
private partial class ModEasyTestPlayer : ModTestPlayer
|
||||
{
|
||||
public int FailuresSuppressed { get; private set; }
|
||||
|
||||
public ModEasyTestPlayer(ModTestData data, bool allowFail)
|
||||
: base(data, allowFail)
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool CheckModsAllowFailure()
|
||||
{
|
||||
bool failureAllowed = GameplayState.Mods.OfType<IApplicableFailOverride>().All(m => m.PerformFail());
|
||||
|
||||
if (!failureAllowed)
|
||||
FailuresSuppressed++;
|
||||
|
||||
return failureAllowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
@@ -18,5 +21,39 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
Autoplay = false,
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSkipToFirstCircleNotSuppressed()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModFreezeFrame(),
|
||||
CreateBeatmap = () => new OsuBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 5000, Position = OsuPlayfield.BASE_SIZE / 2 }
|
||||
}
|
||||
},
|
||||
PassCondition = () => Player.GameplayClockContainer.GameplayStartTime > 0
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSkipToFirstSpinnerNotSuppressed()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModFreezeFrame(),
|
||||
CreateBeatmap = () => new OsuBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Spinner { StartTime = 5000, Position = OsuPlayfield.BASE_SIZE / 2 }
|
||||
}
|
||||
},
|
||||
PassCondition = () => Player.GameplayClockContainer.GameplayStartTime > 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[TestCase("multi-segment-slider")]
|
||||
[TestCase("nan-slider")]
|
||||
[TestCase("1124896")]
|
||||
[TestCase("1341554")]
|
||||
[TestCase("2593923")]
|
||||
[TestCase("801165")]
|
||||
public void Test(string name) => base.Test(name);
|
||||
|
||||
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
|
||||
@@ -34,6 +34,30 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
||||
|
||||
[TestCase(239, "diffcalc-test")]
|
||||
[TestCase(54, "zero-length-sliders")]
|
||||
[TestCase(4, "very-fast-slider")]
|
||||
public void TestOffsetChanges(int expectedMaxCombo, string name)
|
||||
{
|
||||
const double offset_iterations = 400;
|
||||
var beatmap = GetBeatmap(name);
|
||||
|
||||
var attributes = CreateDifficultyCalculator(beatmap).Calculate();
|
||||
double expectedStarRating = attributes.StarRating;
|
||||
|
||||
for (int i = 0; i < offset_iterations; i++)
|
||||
{
|
||||
foreach (var beatmapHitObject in beatmap.Beatmap.HitObjects)
|
||||
beatmapHitObject.StartTime++;
|
||||
|
||||
attributes = CreateDifficultyCalculator(beatmap).Calculate();
|
||||
|
||||
// Platform-dependent math functions (Pow, Cbrt, Exp, etc) may result in minute differences.
|
||||
Assert.That(attributes.StarRating, Is.EqualTo(expectedStarRating).Within(0.00001));
|
||||
Assert.That(attributes.MaxCombo, Is.EqualTo(expectedMaxCombo));
|
||||
}
|
||||
}
|
||||
|
||||
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap);
|
||||
|
||||
protected override Ruleset CreateRuleset() => new OsuRuleset();
|
||||
|
||||
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,941 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
AudioLeadIn: 0
|
||||
PreviewTime: 76429
|
||||
Countdown: 0
|
||||
SampleSet: Soft
|
||||
StackLeniency: 0.2
|
||||
Mode: 0
|
||||
LetterboxInBreaks: 0
|
||||
WidescreenStoryboard: 1
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:5
|
||||
CircleSize:4.3
|
||||
OverallDifficulty:8
|
||||
ApproachRate:9.3
|
||||
SliderMultiplier:2.99999995231628
|
||||
SliderTickRate:1
|
||||
|
||||
[Events]
|
||||
//Background and Video events
|
||||
//Break Periods
|
||||
//Storyboard Layer 0 (Background)
|
||||
//Storyboard Layer 1 (Fail)
|
||||
//Storyboard Layer 2 (Pass)
|
||||
//Storyboard Layer 3 (Foreground)
|
||||
//Storyboard Sound Samples
|
||||
|
||||
[TimingPoints]
|
||||
763,444.444444444444,4,2,1,60,1,0
|
||||
763,-111.111111111111,4,2,1,60,0,0
|
||||
1929,-100,4,2,1,5,0,0
|
||||
1985,-100,4,2,1,60,0,0
|
||||
2040,-100,4,2,1,5,0,0
|
||||
2096,-153.846153846153,4,2,1,60,0,0
|
||||
2429,-133.333333333333,4,2,1,5,0,0
|
||||
2540,-71.4285714285714,4,2,1,70,0,1
|
||||
2985,-100,4,2,1,70,0,1
|
||||
4485,-100,4,2,1,5,0,1
|
||||
4540,-100,4,2,1,70,0,1
|
||||
4707,-100,4,2,1,5,0,1
|
||||
4762,-100,4,2,1,70,0,1
|
||||
4929,-100,4,2,1,5,0,1
|
||||
4985,-100,4,2,1,70,0,1
|
||||
5096,-83.3333333333333,4,2,1,70,0,1
|
||||
5429,-133.333333333333,4,2,1,70,0,1
|
||||
5596,-133.333333333333,4,2,1,70,0,1
|
||||
5652,-133.333333333333,4,2,1,70,0,1
|
||||
5818,-133.333333333333,4,2,1,70,0,1
|
||||
5874,-133.333333333333,4,2,1,70,0,1
|
||||
6040,-133.333333333333,4,2,1,70,0,1
|
||||
6096,-100,4,2,1,70,0,1
|
||||
6540,-100,4,2,1,70,0,1
|
||||
8040,-100,4,2,1,5,0,1
|
||||
8096,-100,4,2,1,70,0,1
|
||||
8262,-100,4,2,1,5,0,1
|
||||
8318,-100,4,2,1,70,0,1
|
||||
8485,-100,4,2,1,5,0,1
|
||||
8540,-133.333333333333,4,2,1,70,0,1
|
||||
8874,-100,4,2,1,5,0,1
|
||||
8985,-100,4,2,1,70,0,1
|
||||
9651,-100,4,2,1,70,0,1
|
||||
10096,-100,4,2,1,70,0,1
|
||||
11596,-100,4,2,1,5,0,1
|
||||
11651,-100,4,2,1,70,0,1
|
||||
11818,-100,4,2,1,5,0,1
|
||||
11873,-100,4,2,1,70,0,1
|
||||
11874,-80,4,2,1,70,0,1
|
||||
12040,-80,4,2,1,5,0,1
|
||||
12096,-80,4,2,1,70,0,1
|
||||
12207,-133.333333333333,4,2,1,70,0,1
|
||||
12429,-100,4,2,1,5,0,1
|
||||
12540,-100,4,2,1,70,0,1
|
||||
12707,-100,4,2,1,70,0,1
|
||||
12763,-100,4,2,1,70,0,1
|
||||
12929,-100,4,2,1,70,0,1
|
||||
12985,-100,4,2,1,70,0,1
|
||||
13429,-300,4,2,1,70,0,1
|
||||
13651,-83.3333333333333,4,2,1,70,0,1
|
||||
13874,-100,4,2,1,70,0,1
|
||||
15151,-100,4,2,1,5,0,1
|
||||
15207,-100,4,2,1,70,0,1
|
||||
15373,-100,4,2,1,5,0,1
|
||||
15429,-100,4,2,1,70,0,1
|
||||
15596,-100,4,2,1,5,0,1
|
||||
15651,-100,4,2,1,70,0,1
|
||||
15985,-100,4,2,1,5,0,1
|
||||
16096,-100,4,2,1,70,0,1
|
||||
16262,-100,4,2,1,70,0,1
|
||||
16318,-83.3333333333333,4,2,1,70,0,1
|
||||
16651,-100,4,2,1,70,0,1
|
||||
16762,-133.333333333333,4,2,1,60,0,0
|
||||
17096,-133.333333333333,4,2,1,5,0,0
|
||||
17207,-200,4,2,1,60,0,0
|
||||
18096,-66.6666666666667,4,2,1,60,0,0
|
||||
18262,-66.6666666666667,4,2,1,5,0,0
|
||||
18318,-66.6666666666667,4,2,1,60,0,0
|
||||
18540,-100,4,2,1,60,0,0
|
||||
18874,-100,4,2,1,5,0,0
|
||||
18985,-100,4,2,1,60,0,0
|
||||
19985,-100,4,2,1,60,0,0
|
||||
20485,-100,4,2,1,5,0,0
|
||||
20540,-100,4,2,1,60,0,0
|
||||
20707,-100,4,2,1,5,0,0
|
||||
20762,-200,4,2,1,60,0,0
|
||||
20985,-100,4,2,1,60,0,0
|
||||
21095,-100,4,2,1,60,0,0
|
||||
21374,-100,4,2,1,5,0,0
|
||||
21429,-100,4,2,1,60,0,0
|
||||
21596,-100,4,2,1,5,0,0
|
||||
21651,-100,4,2,1,60,0,0
|
||||
21818,-100,4,2,1,5,0,0
|
||||
21874,-66.6666666666667,4,2,1,60,0,0
|
||||
21985,-100,4,2,1,60,0,0
|
||||
22096,-100,4,2,1,60,0,0
|
||||
22985,-200,4,2,1,60,0,0
|
||||
23318,-100,4,2,1,60,0,0
|
||||
23429,-100,4,2,1,60,0,0
|
||||
23540,-100,4,2,1,60,0,0
|
||||
23651,-100,4,2,1,60,0,0
|
||||
23762,-100,4,2,1,60,0,0
|
||||
23874,-133.333333333333,4,2,1,60,0,0
|
||||
24208,-133.333333333333,4,2,1,5,0,0
|
||||
24318,-200,4,2,1,5,0,0
|
||||
24319,-200,4,2,1,60,0,0
|
||||
24540,-100,4,2,1,60,0,0
|
||||
24651,-66.6666666666667,4,2,1,60,0,0
|
||||
24874,-100,4,2,1,60,0,0
|
||||
25374,-100,4,2,1,5,0,0
|
||||
25429,-100,4,2,1,60,0,0
|
||||
27096,-100,4,2,1,60,0,0
|
||||
27596,-100,4,2,1,5,0,0
|
||||
27651,-100,4,2,1,60,0,0
|
||||
27818,-100,4,2,1,5,0,0
|
||||
27873,-133.333333333333,4,2,1,60,0,0
|
||||
28096,-100,4,2,1,60,0,0
|
||||
28206,-100,4,2,1,60,0,0
|
||||
28485,-100,4,2,1,5,0,0
|
||||
28540,-100,4,2,1,60,0,0
|
||||
28707,-100,4,2,1,5,0,0
|
||||
28762,-100,4,2,1,60,0,0
|
||||
28929,-100,4,2,1,5,0,0
|
||||
28985,-66.6666666666667,4,2,1,60,0,0
|
||||
29151,-100,4,2,1,60,0,0
|
||||
29207,-100,4,2,1,60,0,0
|
||||
29651,-100,4,2,1,60,0,0
|
||||
30429,-100,4,2,1,60,0,0
|
||||
30540,-58.8235294117647,4,2,1,60,0,0
|
||||
30874,-58.8235294117647,4,2,1,5,0,0
|
||||
30985,-58.8235294117647,4,2,1,60,0,0
|
||||
31040,-100,4,2,1,5,0,0
|
||||
31429,-100,4,2,1,60,0,0
|
||||
32485,-100,4,2,1,60,0,0
|
||||
32540,-100,4,2,1,60,0,0
|
||||
32707,-100,4,2,1,60,0,0
|
||||
32762,-100,4,2,1,60,0,0
|
||||
32985,-100,4,2,1,60,0,0
|
||||
34318,-50,4,2,1,60,0,0
|
||||
34485,-100,4,2,1,5,0,0
|
||||
34540,-100,4,2,1,60,0,0
|
||||
35151,-100,4,2,1,5,0,0
|
||||
35207,-100,4,2,1,60,0,0
|
||||
35374,-100,4,2,1,5,0,0
|
||||
35430,-100,4,2,1,60,0,0
|
||||
35818,-100,4,2,1,5,0,0
|
||||
35874,-200,4,2,1,60,0,0
|
||||
36429,-100,4,2,1,60,0,0
|
||||
37818,-100,4,2,1,5,0,0
|
||||
37874,-100,4,2,1,60,0,0
|
||||
38040,-100,4,2,1,5,0,0
|
||||
38096,-50,4,2,1,60,0,0
|
||||
38151,-100,4,2,1,5,0,0
|
||||
38540,-100,4,2,1,60,0,0
|
||||
39596,-100,4,2,1,5,0,0
|
||||
39651,-100,4,2,1,60,0,0
|
||||
39818,-100,4,2,1,60,0,0
|
||||
39873,-100,4,2,1,60,0,0
|
||||
40096,-100,4,2,1,60,0,0
|
||||
41429,-50,4,2,1,60,0,0
|
||||
41596,-100,4,2,1,5,0,0
|
||||
41651,-100,4,2,1,60,0,0
|
||||
41818,-100,4,2,1,5,0,0
|
||||
41874,-100,4,2,1,60,0,0
|
||||
42040,-100,4,2,1,5,0,0
|
||||
42096,-100,4,2,1,60,0,0
|
||||
44318,-100,4,2,1,60,0,0
|
||||
44762,-83.3333333333333,4,2,1,60,0,0
|
||||
45207,-66.6666666666667,4,2,1,45,0,0
|
||||
45651,-133.333333333333,4,2,1,45,0,0
|
||||
51540,-133.333333333333,4,2,1,50,0,0
|
||||
51651,-133.333333333333,4,2,1,45,0,0
|
||||
52318,-133.333333333333,4,2,1,45,0,0
|
||||
58540,-76.9230769230769,4,2,1,45,0,0
|
||||
58818,-100,4,2,1,45,0,0
|
||||
58874,-111.111111111111,4,2,1,45,0,0
|
||||
59318,-111.111111111111,4,2,1,45,0,0
|
||||
59429,-83.3333333333333,4,2,1,60,0,0
|
||||
59540,-83.3333333333333,4,2,1,5,0,0
|
||||
59874,-100,4,2,1,60,0,0
|
||||
60096,-100,4,2,1,5,0,0
|
||||
60207,-100,4,2,1,60,0,0
|
||||
60707,-100,4,2,1,5,0,0
|
||||
60763,-100,4,2,1,60,0,0
|
||||
60818,-100,4,2,1,5,0,0
|
||||
60874,-100,4,2,1,60,0,0
|
||||
60929,-100,4,2,1,5,0,0
|
||||
60985,-100,4,2,1,60,0,0
|
||||
61040,-100,4,2,1,5,0,0
|
||||
61096,-100,4,2,1,60,0,0
|
||||
61151,-100,4,2,1,5,0,0
|
||||
61207,-100,4,2,1,60,0,0
|
||||
61596,-100,4,2,1,5,0,0
|
||||
61651,-100,4,2,1,60,0,0
|
||||
61762,-83.3333333333333,4,2,1,60,0,0
|
||||
61985,-100,4,2,1,5,0,0
|
||||
62096,-100,4,2,1,60,0,0
|
||||
62151,-100,4,2,1,5,0,0
|
||||
62207,-100,4,2,1,60,0,0
|
||||
62262,-100,4,2,1,5,0,0
|
||||
62318,-100,4,2,1,60,0,0
|
||||
62374,-100,4,2,1,5,0,0
|
||||
62430,-100,4,2,1,60,0,0
|
||||
62485,-100,4,2,1,5,0,0
|
||||
62540,-100,4,2,1,60,0,0
|
||||
62596,-100,4,2,1,5,0,0
|
||||
62651,-100,4,2,1,60,0,0
|
||||
62707,-100,4,2,1,5,0,0
|
||||
62762,-100,4,2,1,60,0,0
|
||||
62818,-100,4,2,1,5,0,0
|
||||
62874,-100,4,2,1,60,0,0
|
||||
62929,-100,4,2,1,60,0,0
|
||||
62930,-100,4,2,1,5,0,0
|
||||
62985,-100,4,2,1,60,0,0
|
||||
63707,-100,4,2,1,5,0,0
|
||||
63762,-100,4,2,1,60,0,0
|
||||
64262,-100,4,2,1,5,0,0
|
||||
64318,-100,4,2,1,60,0,0
|
||||
64485,-100,4,2,1,5,0,0
|
||||
64540,-100,4,2,1,60,0,0
|
||||
64596,-100,4,2,1,5,0,0
|
||||
64651,-100,4,2,1,60,0,0
|
||||
64707,-100,4,2,1,5,0,0
|
||||
64762,-71.4285714285714,4,2,1,60,0,0
|
||||
64929,-71.4285714285714,4,2,1,5,0,0
|
||||
64984,-133.333333333333,4,2,1,60,0,0
|
||||
65151,-133.333333333333,4,2,1,5,0,0
|
||||
65206,-71.4285714285714,4,2,1,60,0,0
|
||||
65374,-71.4285714285714,4,2,1,5,0,0
|
||||
65429,-133.333333333333,4,2,1,60,0,0
|
||||
65596,-133.333333333333,4,2,1,5,0,0
|
||||
65651,-100,4,2,1,60,0,0
|
||||
66540,-66.6666666666667,4,2,1,60,0,0
|
||||
66596,-66.6666666666667,4,2,1,5,0,0
|
||||
66929,-100,4,2,1,5,0,0
|
||||
66985,-200,4,2,1,60,0,0
|
||||
67207,-200,4,2,1,5,0,0
|
||||
67318,-100,4,2,1,60,0,0
|
||||
67818,-100,4,2,1,5,0,0
|
||||
67874,-100,4,2,1,60,0,0
|
||||
67929,-100,4,2,1,5,0,0
|
||||
67985,-100,4,2,1,60,0,0
|
||||
68040,-100,4,2,1,5,0,0
|
||||
68096,-100,4,2,1,60,0,0
|
||||
68151,-100,4,2,1,5,0,0
|
||||
68207,-100,4,2,1,60,0,0
|
||||
68262,-100,4,2,1,5,0,0
|
||||
68318,-100,4,2,1,60,0,0
|
||||
68874,-83.3333333333333,4,2,1,60,0,0
|
||||
69096,-100,4,2,1,60,0,0
|
||||
69097,-100,4,2,1,5,0,0
|
||||
69207,-100,4,2,1,60,0,0
|
||||
69263,-100,4,2,1,5,0,0
|
||||
69319,-100,4,2,1,60,0,0
|
||||
69374,-100,4,2,1,5,0,0
|
||||
69430,-100,4,2,1,60,0,0
|
||||
69486,-100,4,2,1,5,0,0
|
||||
69542,-100,4,2,1,60,0,0
|
||||
69597,-100,4,2,1,5,0,0
|
||||
69651,-100,4,2,1,60,0,0
|
||||
69707,-100,4,2,1,5,0,0
|
||||
69762,-100,4,2,1,60,0,0
|
||||
69818,-100,4,2,1,5,0,0
|
||||
69874,-100,4,2,1,60,0,0
|
||||
69929,-100,4,2,1,5,0,0
|
||||
69985,-100,4,2,1,60,0,0
|
||||
70040,-100,4,2,1,60,0,0
|
||||
70041,-100,4,2,1,5,0,0
|
||||
70096,-100,4,2,1,60,0,0
|
||||
70818,-100,4,2,1,5,0,0
|
||||
70873,-100,4,2,1,60,0,0
|
||||
71207,-71.4285714285714,4,2,1,60,0,0
|
||||
71429,-100,4,2,1,60,0,0
|
||||
71874,-71.4285714285714,4,2,1,60,0,0
|
||||
72041,-71.4285714285714,4,2,1,5,0,0
|
||||
72096,-133.333333333333,4,2,1,60,0,0
|
||||
72263,-133.333333333333,4,2,1,5,0,0
|
||||
72318,-71.4285714285714,4,2,1,60,0,0
|
||||
72485,-71.4285714285714,4,2,1,5,0,0
|
||||
72540,-133.333333333333,4,2,1,60,0,0
|
||||
72985,-66.6666666666667,4,2,1,60,0,0
|
||||
73207,-100,4,2,1,60,0,0
|
||||
73651,-133.333333333333,4,2,1,45,0,0
|
||||
75318,-133.333333333333,4,2,1,5,0,0
|
||||
75429,-133.333333333333,4,2,1,45,0,0
|
||||
76762,-100,4,2,1,45,0,0
|
||||
77096,-100,4,2,1,5,0,0
|
||||
77207,-100,4,2,1,70,0,1
|
||||
77818,-100,4,2,1,5,0,1
|
||||
77874,-100,4,2,1,70,0,1
|
||||
78262,-100,4,2,1,5,0,1
|
||||
78318,-100,4,2,1,70,0,1
|
||||
78540,-83.3333333333333,4,2,1,70,0,1
|
||||
78985,-100,4,2,1,70,0,1
|
||||
79596,-100,4,2,1,5,0,1
|
||||
79651,-100,4,2,1,70,0,1
|
||||
80040,-100,4,2,1,5,0,1
|
||||
80096,-100,4,2,1,70,0,1
|
||||
80318,-83.3333333333333,4,2,1,70,0,1
|
||||
84318,-100,4,2,1,70,0,1
|
||||
84929,-100,4,2,1,5,0,1
|
||||
84985,-100,4,2,1,70,0,1
|
||||
85207,-100,4,2,1,70,0,1
|
||||
85374,-100,4,2,1,5,0,1
|
||||
85429,-100,4,2,1,70,0,1
|
||||
85651,-83.3333333333333,4,2,1,70,0,1
|
||||
86096,-100,4,2,1,70,0,1
|
||||
86707,-100,4,2,1,5,0,1
|
||||
86762,-100,4,2,1,70,0,1
|
||||
88818,-100,4,2,1,5,0,1
|
||||
88874,-100,4,2,1,70,0,1
|
||||
88929,-100,4,2,1,5,0,1
|
||||
88985,-100,4,2,1,70,0,1
|
||||
89040,-100,4,2,1,5,0,1
|
||||
89096,-100,4,2,1,70,0,1
|
||||
92040,-100,4,2,1,5,0,1
|
||||
92096,-100,4,2,1,70,0,1
|
||||
92485,-100,4,2,1,5,0,1
|
||||
92540,-100,4,2,1,70,0,1
|
||||
97651,-200,4,2,1,70,0,1
|
||||
97818,-200,4,2,1,5,0,1
|
||||
97874,-66.6666666666667,4,2,1,70,0,1
|
||||
97985,-66.6666666666667,4,2,1,70,0,1
|
||||
98040,-66.6666666666667,4,2,1,5,0,1
|
||||
98096,-133.333333333333,4,2,1,70,0,1
|
||||
98262,-133.333333333333,4,2,1,5,0,1
|
||||
98318,-66.6666666666667,4,2,1,70,0,1
|
||||
98540,-100,4,2,1,70,0,1
|
||||
99151,-100,4,2,1,5,0,1
|
||||
99207,-100,4,2,1,70,0,1
|
||||
99596,-100,4,2,1,5,0,1
|
||||
99651,-100,4,2,1,70,0,1
|
||||
103040,-100,4,2,1,5,0,1
|
||||
103096,-100,4,2,1,70,0,1
|
||||
103151,-100,4,2,1,5,0,1
|
||||
103207,-100,4,2,1,70,0,1
|
||||
103262,-100,4,2,1,5,0,1
|
||||
103318,-100,4,2,1,70,0,1
|
||||
105207,-83.3333333333333,4,2,1,70,0,1
|
||||
105540,-83.3333333333333,4,2,1,70,0,1
|
||||
105651,-133.333333333333,4,2,1,60,0,0
|
||||
105985,-133.333333333333,4,2,1,5,0,0
|
||||
106096,-200,4,2,1,60,0,0
|
||||
106985,-66.6666666666667,4,2,1,60,0,0
|
||||
107151,-66.6666666666667,4,2,1,5,0,0
|
||||
107207,-66.6666666666667,4,2,1,60,0,0
|
||||
107429,-100,4,2,1,60,0,0
|
||||
107763,-100,4,2,1,5,0,0
|
||||
107874,-100,4,2,1,60,0,0
|
||||
108874,-100,4,2,1,60,0,0
|
||||
109374,-100,4,2,1,5,0,0
|
||||
109429,-100,4,2,1,60,0,0
|
||||
109596,-100,4,2,1,5,0,0
|
||||
109651,-200,4,2,1,60,0,0
|
||||
109929,-100,4,2,1,60,0,0
|
||||
109984,-100,4,2,1,60,0,0
|
||||
110262,-100,4,2,1,5,0,0
|
||||
110318,-100,4,2,1,60,0,0
|
||||
110485,-100,4,2,1,5,0,0
|
||||
110540,-100,4,2,1,60,0,0
|
||||
110707,-100,4,2,1,5,0,0
|
||||
110762,-66.6666666666667,4,2,1,60,0,0
|
||||
110929,-100,4,2,1,60,0,0
|
||||
110985,-133.333333333333,4,2,1,60,0,0
|
||||
111429,-133.333333333333,4,2,1,60,0,0
|
||||
111596,-133.333333333333,4,2,1,60,0,0
|
||||
111651,-133.333333333333,4,2,1,60,0,0
|
||||
111818,-133.333333333333,4,2,1,60,0,0
|
||||
111874,-100,4,2,1,60,0,1
|
||||
112318,-83.3333333333333,4,2,1,60,0,1
|
||||
112429,-100,4,2,1,5,0,0
|
||||
|
||||
|
||||
[Colours]
|
||||
Combo1 : 112,75,180
|
||||
Combo2 : 0,255,255
|
||||
Combo3 : 255,15,117
|
||||
Combo4 : 255,135,15
|
||||
|
||||
[HitObjects]
|
||||
309,230,763,37,0,3:0:0:0:
|
||||
485,146,985,2,0,L|406:167,1,67.4999968671799,8|0,3:0|0:0,0:0:0:0:
|
||||
374,249,1207,2,0,L|299:227,1,67.4999968671799,8|0,3:0|0:0,0:0:0:0:
|
||||
196,91,1429,2,0,L|191:44,3,33.7499984335899,0|0|0|0,3:0|3:0|3:0|3:0,0:0:0:0:
|
||||
124,173,1651,2,0,L|131:222,2,44.9999979114532,0|0|0,3:0|3:0|3:0,0:0:0:0:
|
||||
221,284,1874,2,0,L|213:208,1,67.4999968671799,0|0,3:0|3:0,0:0:0:0:
|
||||
292,86,2096,38,0,L|310:234,1,146.249990980625,12|0,3:0|0:0,0:0:0:0:
|
||||
314,328,2540,38,0,B|280:359|280:359|230:320|252:242|313:230,1,209.999990253448,0|0,3:0|0:0,0:0:0:0:
|
||||
421,300,2874,1,0,0:0:0:0:
|
||||
421,300,2985,2,0,P|461:288|491:253,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
|
||||
309,231,3207,2,0,P|297:190|305:153,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
394,22,3429,5,0,3:0:0:0:
|
||||
461,72,3540,2,0,B|477:103|477:103|461:148,1,74.999998807907,0|4,0:0|0:0,0:0:0:0:
|
||||
378,183,3762,2,0,L|206:157,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
|
||||
229,161,4096,2,0,P|227:202|211:250,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
61,384,4318,38,0,P|101:359|134:322,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
317,310,4540,2,0,P|267:305|226:288,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
141,110,4762,2,0,B|121:175|152:226|152:226|152:202|161:183,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
|
||||
155,196,5096,6,0,P|67:211|79:286,1,179.999991645813,0|0,0:0|0:0,0:0:0:0:
|
||||
212,366,5429,38,0,P|207:335|174:281,1,56.2500012516975,4|0,0:0|0:0,0:0:0:0:
|
||||
206,286,5651,2,0,P|236:297|299:295,1,56.2500012516975,8|0,3:0|0:0,0:0:0:0:
|
||||
281,321,5874,2,0,P|257:340|227:396,1,56.2500012516975,4|0,0:0|0:0,0:0:0:0:
|
||||
124,246,6096,6,0,P|198:198|277:232,1,149.999997615814,0|0,3:0|0:0,0:0:0:0:
|
||||
253,211,6429,1,0,0:0:0:0:
|
||||
276,99,6540,2,0,P|335:139|369:215,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
|
||||
368,208,6874,1,0,0:0:0:0:
|
||||
430,96,6985,37,0,3:0:0:0:
|
||||
497,147,7096,2,0,P|507:189|488:244,1,74.999998807907,0|4,0:0|0:0,0:0:0:0:
|
||||
414,379,7318,2,0,B|383:322|421:267|421:267|421:308,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
|
||||
421,298,7651,2,0,P|378:312|336:304,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
270,170,7874,6,0,P|275:228|236:278,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
94,300,8096,2,0,P|133:263|208:274,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
261,374,8318,2,0,L|176:365,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
|
||||
38,377,8540,2,0,L|55:197,1,168.750003755093,4|0,0:0|0:0,0:0:0:0:
|
||||
123,25,8985,38,0,L|132:110,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
217,242,9207,2,0,L|237:168,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
|
||||
48,92,9429,5,4,0:0:0:0:
|
||||
63,176,9540,1,0,0:0:0:0:
|
||||
83,259,9651,38,0,P|167:223|231:255,1,149.999997615814,0|0,3:0|0:0,0:0:0:0:
|
||||
274,312,9985,1,0,0:0:0:0:
|
||||
274,312,10096,2,0,L|354:292,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
|
||||
459,225,10318,2,0,L|375:204,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
269,107,10540,1,0,3:0:0:0:
|
||||
276,54,10651,1,0,0:0:0:0:
|
||||
313,17,10762,1,4,0:0:0:0:
|
||||
363,9,10874,1,0,0:0:0:0:
|
||||
363,9,11096,5,0,0:0:0:0:
|
||||
432,68,11207,2,0,P|444:107|425:154,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
309,252,11429,38,0,P|297:195|321:158,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
450,316,11651,2,0,L|361:312,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
160,341,11874,2,0,B|187:380|187:380|233:309|177:235,1,187.499997019767,8|4,3:0|0:0,0:0:0:0:
|
||||
116,200,12207,6,0,P|52:224|122:264,1,168.750003755093,0|4,0:0|0:0,0:0:0:0:
|
||||
297,91,12762,37,8,3:0:0:0:
|
||||
276,44,12874,1,0,0:0:0:0:
|
||||
226,27,12985,1,4,0:0:0:0:
|
||||
187,63,13096,1,0,0:0:0:0:
|
||||
196,115,13207,1,0,0:0:0:0:
|
||||
376,144,13429,2,0,L|378:121,2,16.6666664017571,0|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||
436,220,13651,6,0,B|395:211|373:164|373:164|332:208|264:185,1,179.999991645813,8|4,3:0|0:0,0:0:0:0:
|
||||
276,44,13985,1,0,0:0:0:0:
|
||||
196,115,14096,38,0,L|139:124,4,37.4999994039535,0|0|0|0|4,3:0|0:0|0:0|0:0|0:0,0:0:0:0:
|
||||
82,69,14429,1,0,0:0:0:0:
|
||||
106,190,14540,2,0,L|126:276,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
|
||||
218,383,14762,2,0,L|234:309,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
26,231,14985,5,0,3:0:0:0:
|
||||
253,202,15207,37,0,0:0:0:0:
|
||||
331,271,15318,1,0,0:0:0:0:
|
||||
233,309,15429,1,8,3:0:0:0:
|
||||
389,73,15651,6,0,P|410:22|447:112,1,224.999996423721,4|0,3:0|0:0,0:0:0:0:
|
||||
391,165,16096,1,0,0:0:0:0:
|
||||
377,177,16207,1,0,0:0:0:0:
|
||||
365,187,16318,38,0,B|253:261|221:119|94:192,1,269.999987468719,0|0,0:0|0:0,0:0:0:0:
|
||||
73,319,16762,22,0,P|133:336|116:236,1,168.750003755093,4|0,3:0|0:0,0:0:0:0:
|
||||
139,258,17207,6,0,P|138:315|69:283,1,112.500002503395,8|0,3:0|0:0,0:0:0:0:
|
||||
92,323,17762,37,0,0:0:0:0:
|
||||
43,245,17874,1,4,0:0:0:0:
|
||||
4,322,17985,1,0,0:0:0:0:
|
||||
133,245,18096,1,0,3:0:0:0:
|
||||
29,105,18318,6,0,L|38:40,3,56.2500012516975,4|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
|
||||
50,30,18540,38,0,P|111:56|193:25,1,149.999997615814,0|0,3:0|0:0,0:0:0:0:
|
||||
240,120,18985,2,0,P|328:91|394:125,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
|
||||
409,213,19318,2,0,B|377:226|377:226|243:200,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
|
||||
119,187,19651,2,0,L|127:286,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
179,338,19874,1,8,3:0:0:0:
|
||||
45,307,19985,6,0,L|3:297,2,37.4999994039535,0|0|4,0:0|0:0|0:0,0:0:0:0:
|
||||
103,380,20207,1,0,3:0:0:0:
|
||||
212,257,20318,38,0,P|233:218|231:171,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
111,118,20540,1,4,0:0:0:0:
|
||||
111,118,20762,6,0,L|197:109,1,74.999998807907,8|4,3:0|0:0,0:0:0:0:
|
||||
256,18,21096,37,0,0:0:0:0:
|
||||
337,121,21207,2,0,P|350:60|403:16,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
|
||||
384,26,21429,2,0,P|406:86|465:122,1,112.49999821186,0|0,0:0|0:0,0:0:0:0:
|
||||
443,114,21651,2,0,P|377:105|327:131,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
|
||||
352,223,21874,6,0,B|369:230|369:230|391:228|391:228|416:239|416:239|440:235|440:235|462:244|462:244|489:249,1,112.500002503395,4|0,0:0|0:0,0:0:0:0:
|
||||
322,343,22096,37,0,3:0:0:0:
|
||||
259,270,22207,2,0,P|223:276|182:263,2,74.999998807907,0|4|0,0:0|0:0|0:0,0:0:0:0:
|
||||
86,360,22540,5,8,3:0:0:0:
|
||||
15,295,22651,2,0,L|0:201,2,74.999998807907,0|4|0,0:0|0:0|0:0,0:0:0:0:
|
||||
94,384,22985,38,0,P|118:328|112:277,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
|
||||
0,211,23429,22,0,L|76:196,1,74.999998807907,12|0,3:0|0:0,0:0:0:0:
|
||||
215,134,23651,2,0,L|114:110,1,74.999998807907,12|0,3:0|0:0,0:0:0:0:
|
||||
33,124,23874,22,0,L|43:2,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
|
||||
150,269,24318,2,0,L|162:194,1,74.999998807907,0|4,3:0|0:0,0:0:0:0:
|
||||
229,134,24651,6,0,L|386:164,1,112.500002503395,12|0,3:0|0:0,0:0:0:0:
|
||||
486,268,24874,37,0,0:0:0:0:
|
||||
410,119,24985,1,4,0:0:0:0:
|
||||
381,213,25096,1,0,0:0:0:0:
|
||||
512,120,25207,1,0,3:0:0:0:
|
||||
247,36,25429,6,0,L|191:25,3,37.4999994039535,4|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
|
||||
185,24,25651,2,0,B|145:72|145:72|174:164,1,149.999997615814,0|0,3:0|0:0,0:0:0:0:
|
||||
253,219,26096,2,0,B|281:311|281:311|228:382,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
|
||||
100,363,26429,38,0,L|259:354,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
|
||||
404,262,26762,1,4,0:0:0:0:
|
||||
390,352,26874,1,0,0:0:0:0:
|
||||
314,295,26985,1,8,3:0:0:0:
|
||||
425,256,27096,6,0,L|492:246,2,37.4999994039535,0|0|4,0:0|0:0|0:0,0:0:0:0:
|
||||
329,216,27318,1,0,3:0:0:0:
|
||||
193,177,27429,38,0,L|266:161,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
322,107,27651,1,4,0:0:0:0:
|
||||
322,107,27874,2,0,L|310:238,1,112.500002503395,8|4,3:0|0:0,0:0:0:0:
|
||||
110,299,28207,5,0,0:0:0:0:
|
||||
164,231,28318,2,0,B|168:303|168:303|121:338,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
|
||||
30,284,28540,2,0,B|90:244|90:244|144:267,1,112.49999821186,0|0,0:0|0:0,0:0:0:0:
|
||||
148,371,28762,2,0,B|83:338|83:338|76:280,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
|
||||
194,201,28985,38,0,B|207:210|207:210|227:210|227:210|243:217|243:217|265:218|265:218|282:227|282:227|305:225|305:225|325:238,1,112.500002503395,4|0,0:0|0:0,0:0:0:0:
|
||||
492,114,29207,6,0,P|445:136|410:138,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
324,102,29429,2,0,P|291:68|280:29,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
418,17,29651,1,8,3:0:0:0:
|
||||
495,201,29874,1,4,3:2:0:0:
|
||||
221,136,30096,37,0,3:0:0:0:
|
||||
299,188,30207,2,0,B|316:251|316:251|271:352,1,149.999997615814,4|0,3:0|0:0,0:0:0:0:
|
||||
115,334,30540,6,0,P|11:252|167:266,1,382.500001215934,0|0,0:0|0:0,0:0:0:0:
|
||||
216,326,30985,38,0,L|304:331,1,63.7500002026557,4|0,3:0|0:0,0:0:0:0:
|
||||
280,330,31429,6,0,L|293:241,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
|
||||
426,252,31651,2,0,L|439:163,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
|
||||
253,158,31874,37,0,3:0:0:0:
|
||||
258,132,31985,1,0,0:0:0:0:
|
||||
337,111,32096,5,4,0:0:0:0:
|
||||
341,85,32207,1,0,0:0:0:0:
|
||||
271,30,32318,38,0,B|212:42|212:42|141:19,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
|
||||
163,26,32540,2,0,L|144:181,1,149.999997615814,4|0,0:0|3:0,0:0:0:0:
|
||||
445,343,32985,22,0,B|439:234|439:234|384:269,1,149.999997615814,4|8,0:0|3:0,0:0:0:0:
|
||||
240,257,33429,2,0,B|263:148|263:148|291:205,1,149.999997615814,4|0,0:0|3:0,0:0:0:0:
|
||||
68,333,33874,2,0,B|83:233|83:233|41:256,1,149.999997615814,4|8,0:0|3:0,0:0:0:0:
|
||||
344,347,34318,22,0,B|368:372|368:372|455:355|455:355|472:308,1,149.999997615814,4|0,0:0|0:0,0:0:0:0:
|
||||
452,255,34540,2,0,B|389:212|389:212|332:273,1,149.999997615814,0|4,3:0|0:0,0:0:0:0:
|
||||
256,220,34874,5,0,0:0:0:0:
|
||||
256,220,34985,2,0,B|256:128,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
|
||||
256,70,35207,2,0,B|256:162,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
112,312,35429,37,0,3:0:0:0:
|
||||
60,255,35540,2,0,B|123:212|123:212|180:273,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
|
||||
169,350,35874,6,0,B|144:375|144:375|57:358|57:358|40:311,1,149.999997615814,8|0,3:0|3:0,0:0:0:0:
|
||||
62,169,36429,6,0,L|76:267,2,74.999998807907,0|4|0,0:0|0:0|0:0,0:0:0:0:
|
||||
134,61,36762,1,8,3:0:0:0:
|
||||
201,113,36874,2,0,L|215:211,2,74.999998807907,0|4|0,0:0|0:0|0:0,0:0:0:0:
|
||||
298,272,37207,6,0,L|315:184,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
330,114,37429,1,4,0:0:0:0:
|
||||
446,176,37540,2,0,B|404:214|404:214|307:197,1,149.999997615814,4|0,3:0|0:0,0:0:0:0:
|
||||
231,240,37874,2,0,P|223:199|231:162,1,74.999998807907,4|0,3:0|0:0,0:0:0:0:
|
||||
325,285,38096,6,0,L|154:300,1,149.999997615814,4|0,3:0|0:0,0:0:0:0:
|
||||
175,298,38540,6,0,L|163:396,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
|
||||
75,208,38762,2,0,L|63:306,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
|
||||
233,74,38985,37,0,3:0:0:0:
|
||||
231,98,39096,1,0,0:0:0:0:
|
||||
156,139,39207,5,4,0:0:0:0:
|
||||
155,165,39318,1,0,0:0:0:0:
|
||||
227,215,39429,38,0,P|282:209|352:230,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
|
||||
336,222,39651,2,0,L|366:67,1,149.999997615814,4|0,0:0|3:0,0:0:0:0:
|
||||
81,35,40096,22,0,B|82:105|82:105|118:136|118:136|132:89,1,149.999997615814,4|8,0:0|3:0,0:0:0:0:
|
||||
272,158,40540,2,0,B|270:228|270:228|234:259|234:259|220:212,1,149.999997615814,4|0,0:0|3:0,0:0:0:0:
|
||||
423,36,40985,2,0,B|400:102|400:102|423:143|423:143|453:104,1,149.999997615814,4|8,0:0|3:0,0:0:0:0:
|
||||
512,278,41429,6,0,P|415:258|361:293,1,149.999997615814,4|0,0:0|0:0,0:0:0:0:
|
||||
359,302,41651,6,0,B|320:264|320:264|310:187,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
|
||||
322,190,41874,2,0,L|449:171,1,112.49999821186,4|0,0:0|0:0,0:0:0:0:
|
||||
443,159,42096,1,8,3:0:0:0:
|
||||
240,52,42318,6,0,B|255:79|255:79|241:135,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
177,166,42540,1,0,3:0:0:0:
|
||||
163,151,42651,2,0,B|161:207|161:207|192:240|192:240|189:299,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
|
||||
131,365,42985,2,0,P|198:322|280:345,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
|
||||
335,377,43318,1,0,0:0:0:0:
|
||||
442,239,43429,38,0,B|456:178|456:178|422:136|422:136|427:68|427:68|449:112,1,224.999996423721,0|0,3:0|0:0,0:0:0:0:
|
||||
444,103,43874,2,0,P|402:118|356:120,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
|
||||
249,28,44096,2,0,P|295:35|324:48,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
|
||||
364,201,44318,5,0,0:0:0:0:
|
||||
332,195,44429,1,0,0:0:0:0:
|
||||
251,135,44540,37,0,0:0:0:0:
|
||||
281,123,44651,1,0,0:0:0:0:
|
||||
332,195,44762,6,0,B|356:269|324:333|324:333|303:293,1,179.999991645813,4|0,0:3|0:0,0:0:0:0:
|
||||
61,25,45207,38,0,L|88:158,1,112.500002503395,0|0,3:0|0:0,0:0:0:0:
|
||||
84,136,45651,1,8,3:0:0:0:
|
||||
84,136,46096,1,0,3:0:0:0:
|
||||
176,33,46207,2,0,L|164:103,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
219,207,46429,2,0,L|232:152,1,56.2500012516975,0|8,0:0|3:0,0:0:0:0:
|
||||
312,65,46651,1,0,0:0:0:0:
|
||||
312,65,46762,2,0,L|398:94,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
512,176,46985,5,0,3:0:0:0:
|
||||
421,192,47096,1,0,0:0:0:0:
|
||||
421,192,47429,1,8,3:0:0:0:
|
||||
402,357,47651,37,0,0:0:0:0:
|
||||
394,277,47762,1,0,0:0:0:0:
|
||||
328,324,47874,1,0,3:0:0:0:
|
||||
328,324,48318,1,8,3:0:0:0:
|
||||
110,357,48540,5,0,0:0:0:0:
|
||||
118,277,48651,1,0,0:0:0:0:
|
||||
184,324,48763,1,0,3:0:0:0:
|
||||
110,357,48874,1,0,0:0:0:0:
|
||||
110,357,49207,1,8,3:0:0:0:
|
||||
110,357,49651,1,0,3:0:0:0:
|
||||
0,283,49762,38,0,P|41:301|97:295,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
188,219,49985,2,0,P|168:236|137:246,1,56.2500012516975,0|8,0:0|3:0,0:0:0:0:
|
||||
49,137,50207,1,0,0:0:0:0:
|
||||
49,137,50318,2,0,P|65:184|93:205,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
107,67,50540,5,0,3:0:0:0:
|
||||
32,15,50651,1,0,0:0:0:0:
|
||||
32,15,50985,1,8,3:0:0:0:
|
||||
265,114,51207,37,0,0:0:0:0:
|
||||
254,196,51318,1,0,0:0:0:0:
|
||||
241,279,51429,1,0,3:0:0:0:
|
||||
241,279,51651,1,0,0:0:0:0:
|
||||
336,207,51762,6,0,P|397:191|371:274,1,168.750003755093,0|0,0:0|0:0,0:0:0:0:
|
||||
83,206,52318,5,0,3:0:0:0:
|
||||
83,206,52429,2,0,L|101:260,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
40,383,52651,2,0,P|70:355|90:324,1,56.2500012516975,0|8,0:0|3:0,0:0:0:0:
|
||||
214,334,52874,1,0,0:0:0:0:
|
||||
214,334,52985,2,0,P|171:322|140:304,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
151,160,53207,5,0,3:0:0:0:
|
||||
188,135,53318,1,0,0:0:0:0:
|
||||
232,129,53429,1,0,0:0:0:0:
|
||||
273,146,53540,1,0,0:0:0:0:
|
||||
339,198,53651,37,8,3:0:0:0:
|
||||
383,199,53762,1,0,0:0:0:0:
|
||||
426,185,53874,1,0,0:0:0:0:
|
||||
450,147,53985,1,0,0:0:0:0:
|
||||
444,61,54096,6,0,P|414:28|377:15,1,56.2500012516975,0|0,3:0|0:0,0:0:0:0:
|
||||
301,28,54318,2,0,P|268:48|255:77,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
189,271,54540,38,0,P|209:222|204:198,1,56.2500012516975,8|0,3:0|0:0,0:0:0:0:
|
||||
186,114,54762,2,0,P|152:74|124:68,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
27,137,54985,5,0,3:0:0:0:
|
||||
34,167,55096,1,0,0:0:0:0:
|
||||
122,204,55207,37,0,0:0:0:0:
|
||||
116,178,55318,1,0,0:0:0:0:
|
||||
48,249,55429,5,8,3:0:0:0:
|
||||
54,274,55540,1,0,0:0:0:0:
|
||||
124,329,55651,38,0,P|157:326|200:310,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
320,185,55874,5,0,3:0:0:0:
|
||||
287,175,55985,1,0,0:0:0:0:
|
||||
254,181,56096,2,0,P|258:221|264:241,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
337,347,56318,2,0,P|348:321|350:293,1,56.2500012516975,8|0,3:0|0:0,0:0:0:0:
|
||||
418,197,56540,37,0,0:0:0:0:
|
||||
418,197,56651,2,0,L|492:180,1,56.2500012516975,0|0,0:0|3:0,0:0:0:0:
|
||||
329,114,56874,2,0,L|262:94,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
436,59,57096,6,0,L|413:126,1,56.2500012516975,0|8,0:0|3:0,0:0:0:0:
|
||||
332,194,57318,2,0,L|353:259,2,56.2500012516975,0|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||
202,194,57651,37,0,3:0:0:0:
|
||||
224,233,57762,1,0,0:0:0:0:
|
||||
222,279,57874,1,0,0:0:0:0:
|
||||
193,314,57985,1,0,0:0:0:0:
|
||||
144,244,58096,5,0,0:0:0:0:
|
||||
127,214,58207,1,0,0:0:0:0:
|
||||
126,180,58318,1,0,0:0:0:0:
|
||||
139,149,58429,1,0,0:0:0:0:
|
||||
224,113,58540,38,0,B|262:88|235:70|189:83|189:83|224:138|194:193,1,194.999987974167
|
||||
299,319,58874,1,0,0:0:0:0:
|
||||
299,319,58985,2,0,B|316:283|314:237|314:237|278:226|278:226|320:243|359:227,1,202.49999060154,4|0,0:0|0:0,0:0:0:0:
|
||||
428,181,59429,22,0,P|454:129|399:4,1,179.999991645813,0|0,3:0|0:0,0:0:0:0:
|
||||
418,18,59874,2,0,L|373:15,6,24.9999996026357,8|0|0|0|0|0|4,3:0|0:0|0:0|0:0|0:0|0:0|0:0,0:0:0:0:
|
||||
428,181,60207,5,0,0:0:0:0:
|
||||
352,209,60318,1,0,3:0:0:0:
|
||||
278,177,60429,1,0,0:0:0:0:
|
||||
208,225,60540,2,0,L|222:267,2,37.4999994039535,4|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||
71,144,60762,38,0,L|65:109,3,24.9999996026357,8|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
|
||||
145,86,60985,1,4,0:0:0:0:
|
||||
163,127,61096,1,0,0:0:0:0:
|
||||
161,171,61207,1,0,3:0:0:0:
|
||||
136,208,61318,1,0,0:0:0:0:
|
||||
99,231,61429,2,0,B|91:279|91:279|117:314,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
177,378,61651,5,8,3:0:0:0:
|
||||
177,378,61762,2,0,B|231:371|231:371|272:326|272:326|345:319,1,179.999991645813
|
||||
417,293,62096,1,0,3:0:0:0:
|
||||
438,263,62207,1,0,0:0:0:0:
|
||||
436,225,62318,1,4,0:0:0:0:
|
||||
412,196,62429,1,0,0:0:0:0:
|
||||
320,172,62540,6,0,P|307:192|291:204,1,37.4999994039535,8|0,3:0|0:0,0:0:0:0:
|
||||
291,147,62651,2,0,P|274:156|245:153,1,37.4999994039535,0|0,0:0|0:0,0:0:0:0:
|
||||
276,114,62762,2,0,P|250:107|234:94,1,37.4999994039535,4|0,0:0|0:0,0:0:0:0:
|
||||
283,81,62874,2,0,P|265:61|260:45,1,37.4999994039535,0|0,0:0|0:0,0:0:0:0:
|
||||
365,31,62985,38,0,P|398:44|442:49,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
512,169,63207,2,0,P|466:163|421:176,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
350,107,63429,1,8,3:0:0:0:
|
||||
293,237,63540,38,0,L|276:158,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
|
||||
428,269,63762,2,0,B|373:275|373:275|338:249|338:249|267:255,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
|
||||
191,318,64096,2,0,B|182:355|182:355|212:395,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
192,186,64318,5,8,3:0:0:0:
|
||||
135,253,64429,2,0,L|56:270,2,74.999998807907,0|4|0,0:0|0:0|0:0,0:0:0:0:
|
||||
24,136,64762,38,0,P|69:76|158:75,1,157.499992690086,0|0,3:0|0:0,0:0:0:0:
|
||||
160,80,64985,6,0,P|193:102|255:102,1,84.3750018775463,4|0,0:0|0:0,0:0:0:0:
|
||||
276,34,65207,38,0,L|290:212,1,157.499992690086,8|0,3:0|0:0,0:0:0:0:
|
||||
291,219,65429,6,0,L|311:132,1,84.3750018775463,4|0,0:0|0:0,0:0:0:0:
|
||||
381,111,65651,38,0,B|418:126|418:126|460:126,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
221,163,65874,2,0,B|186:143|186:143|139:153,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
41,231,66096,1,8,3:0:0:0:
|
||||
49,267,66207,1,0,0:0:0:0:
|
||||
56,303,66318,1,4,0:0:0:0:
|
||||
67,288,66429,1,4,0:0:0:0:
|
||||
77,270,66540,6,0,P|171:255|72:350,1,337.500007510185,0|0,0:0|0:0,0:0:0:0:
|
||||
95,356,66985,38,0,L|185:343,1,74.999998807907,8|4,3:0|0:0,0:0:0:0:
|
||||
274,286,67318,6,0,B|289:324|289:324|268:378,1,74.999998807907,0|0,0:0|3:0,0:0:0:0:
|
||||
191,227,67540,1,0,0:0:0:0:
|
||||
255,168,67651,2,0,L|264:116,2,37.4999994039535,4|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||
147,83,67874,2,0,L|154:108,3,24.9999996026357,8|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
|
||||
80,148,68096,38,0,L|98:224,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
125,356,68318,1,0,3:0:0:0:
|
||||
0,319,68429,1,0,0:0:0:0:
|
||||
0,319,68540,2,0,L|76:294,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
277,219,68762,5,8,3:0:0:0:
|
||||
277,219,68874,2,0,B|327:199|327:199|293:138|197:173,1,179.999991645813,0|0,0:0|0:0,0:0:0:0:
|
||||
157,273,69207,37,0,3:0:0:0:
|
||||
175,316,69318,1,0,0:0:0:0:
|
||||
212,334,69429,1,4,0:0:0:0:
|
||||
254,333,69540,1,0,0:0:0:0:
|
||||
332,268,69651,38,0,P|333:237|343:213,1,37.4999994039535,8|0,3:0|0:0,0:0:0:0:
|
||||
373,265,69762,2,0,P|386:239|404:232,1,37.4999994039535,0|0,0:0|0:0,0:0:0:0:
|
||||
413,284,69874,2,0,P|430:269|454:269,1,37.4999994039535,4|0,0:0|0:0,0:0:0:0:
|
||||
433,318,69985,2,0,P|452:320|474:337,1,37.4999994039535,4|0,0:0|0:0,0:0:0:0:
|
||||
401,384,70096,6,0,P|353:378|319:346,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
251,251,70318,2,0,P|240:196|260:154,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
401,18,70540,1,8,3:0:0:0:
|
||||
401,18,70651,2,0,P|409:54|398:90,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
|
||||
327,193,70874,2,0,L|304:45,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
|
||||
290,26,71207,6,0,L|308:144,1,104.999995126724,0|0,0:0|0:0,0:0:0:0:
|
||||
272,302,71429,2,0,L|187:288,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
|
||||
33,217,71651,37,4,0:0:0:0:
|
||||
27,187,71762,1,4,0:0:0:0:
|
||||
20,157,71874,2,0,B|103:140|103:140|162:58,1,157.499992690086,0|0,3:0|0:0,0:0:0:0:
|
||||
145,82,72096,6,0,L|218:75,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
336,136,72318,38,0,P|331:213|231:208,1,157.499992690086
|
||||
263,232,72540,6,0,L|278:300,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
183,384,72762,2,0,L|172:307,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
37,140,72985,38,0,B|10:168|10:168|17:204|17:204|54:220|54:220|89:196|89:196|87:157|87:157|57:138,1,225.00000500679
|
||||
275,372,73651,6,0,P|320:352|387:369,1,112.500002503395
|
||||
380,364,74096,2,0,L|436:358,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
495,271,74318,2,0,L|424:282,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
339,270,74540,1,0,0:0:0:0:
|
||||
339,270,74651,1,0,0:0:0:0:
|
||||
339,270,74762,2,0,L|329:196,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
408,46,74985,38,0,L|392:120,1,56.2500012516975
|
||||
220,230,75207,2,0,L|209:156,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
282,7,75429,37,0,0:0:0:0:
|
||||
300,98,75540,1,0,0:0:0:0:
|
||||
197,25,75651,5,0,0:0:0:0:
|
||||
222,103,75762,1,0,0:0:0:0:
|
||||
126,69,75874,5,0,0:0:0:0:
|
||||
153,134,75985,1,0,0:0:0:0:
|
||||
76,145,76096,5,0,0:0:0:0:
|
||||
116,179,76207,1,0,0:0:0:0:
|
||||
70,222,76318,5,0,0:0:0:0:
|
||||
111,222,76429,1,0,0:0:0:0:
|
||||
134,253,76540,6,0,P|135:298|126:314,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
21,384,76762,2,0,P|124:354|260:391,1,224.999996423721,0|0,0:0|0:0,0:0:0:0:
|
||||
384,366,77207,22,0,L|394:268,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
499,62,77429,2,0,L|486:135,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
507,237,77651,2,0,P|450:231|388:184,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
|
||||
404,203,77874,2,0,L|313:217,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
113,212,78096,6,0,P|128:267|111:328,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
|
||||
115,319,78318,2,0,L|213:340,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
274,371,78540,38,0,L|257:186,1,179.999991645813,8|0,3:0|0:0,0:0:0:0:
|
||||
128,139,78874,1,0,0:0:0:0:
|
||||
128,139,78985,6,0,L|230:128,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
365,34,79207,37,4,0:0:0:0:
|
||||
430,114,79318,1,0,0:0:0:0:
|
||||
361,184,79429,2,0,P|304:170|277:110,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
|
||||
278,126,79651,2,0,L|189:133,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
64,263,79874,6,0,B|37:230|37:230|50:143,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
|
||||
66,119,80096,2,0,L|80:210,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
71,361,80318,38,0,B|135:350|135:350|182:305|182:305|243:297,1,179.999991645813,8|0,3:0|0:0,0:0:0:0:
|
||||
302,247,80651,1,0,0:0:0:0:
|
||||
222,211,80762,1,0,3:0:0:0:
|
||||
478,344,80985,5,4,0:0:0:0:
|
||||
491,309,81096,5,0,0:0:0:0:
|
||||
498,265,81207,5,8,3:0:0:0:
|
||||
485,223,81318,5,0,0:0:0:0:
|
||||
458,179,81429,5,4,0:0:0:0:
|
||||
418,147,81540,5,0,0:0:0:0:
|
||||
352,126,81651,5,0,3:0:0:0:
|
||||
281,149,81762,5,0,0:0:0:0:
|
||||
239,221,81874,5,4,0:0:0:0:
|
||||
159,262,81985,5,0,0:0:0:0:
|
||||
66,234,82096,5,8,3:0:0:0:
|
||||
11,145,82207,5,0,0:0:0:0:
|
||||
55,33,82318,5,4,0:0:0:0:
|
||||
273,44,82540,37,0,3:0:0:0:
|
||||
320,103,82651,1,0,0:0:0:0:
|
||||
394,118,82762,1,4,0:0:0:0:
|
||||
468,100,82874,1,0,0:0:0:0:
|
||||
507,36,82985,1,8,3:0:0:0:
|
||||
495,19,83207,5,4,0:0:0:0:
|
||||
335,83,83318,1,0,0:0:0:0:
|
||||
453,81,83429,1,0,3:0:0:0:
|
||||
283,24,83540,2,0,P|196:37|141:120,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
|
||||
60,238,83874,1,8,3:0:0:0:
|
||||
21,164,83985,2,0,P|59:149|175:193,1,149.999997615814,0|8,0:0|3:0,0:0:0:0:
|
||||
252,206,84318,38,0,P|271:160|264:125,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
139,257,84540,2,0,P|131:302|149:340,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
240,379,84762,2,0,B|330:360|330:360|298:344,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
|
||||
312,351,84985,2,0,P|279:321|270:287,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
359,165,85207,6,0,B|389:202|389:202|368:282,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
|
||||
373,265,85429,2,0,L|454:282,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
498,139,85651,38,0,P|446:120|396:0,1,179.999991645813,8|0,3:0|0:0,0:0:0:0:
|
||||
394,13,85985,1,0,0:0:0:0:
|
||||
301,92,86096,6,0,L|214:83,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
66,66,86318,1,4,0:0:0:0:
|
||||
13,136,86429,1,0,0:0:0:0:
|
||||
72,193,86540,2,0,P|120:210|190:178,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
|
||||
176,192,86762,2,0,P|154:237|160:288,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
309,370,86985,37,0,3:0:0:0:
|
||||
359,310,87096,1,0,0:0:0:0:
|
||||
283,297,87207,2,0,L|203:318,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
4,203,87429,2,0,B|55:211|55:211|82:255|82:255|134:266,1,149.999997615814,8|0,3:0|0:0,0:0:0:0:
|
||||
238,217,87762,1,0,0:0:0:0:
|
||||
183,120,87874,6,0,L|89:111,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
98,33,88096,2,0,L|23:26,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
306,182,88318,38,0,L|400:173,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
|
||||
391,95,88540,2,0,L|465:88,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
232,28,88762,2,0,L|220:92,1,37.4999994039535,0|0,3:0|0:0,0:0:0:0:
|
||||
243,39,88874,2,0,L|231:103,1,37.4999994039535,0|0,0:0|0:0,0:0:0:0:
|
||||
256,50,88985,2,0,L|251:87,1,37.4999994039535,4|0,0:0|0:0,0:0:0:0:
|
||||
485,87,89207,6,0,L|493:51,3,37.4999994039535,8|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
|
||||
396,120,89429,2,0,L|411:197,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
471,317,89651,38,0,P|411:299|320:336,1,149.999997615814,0|4,3:0|0:0,0:0:0:0:
|
||||
61,239,90096,2,0,P|121:221|212:258,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
|
||||
367,21,90540,6,0,P|336:57|328:104,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
163,96,90762,2,0,P|194:132|202:179,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
|
||||
190,346,90985,37,8,3:0:0:0:
|
||||
328,272,91096,1,0,0:0:0:0:
|
||||
154,272,91207,5,8,3:0:0:0:
|
||||
365,338,91318,1,0,0:0:0:0:
|
||||
257,382,91429,38,0,B|290:333|224:286|269:219,1,149.999997615814,4|4,3:0|0:0,0:0:0:0:
|
||||
325,196,91762,1,0,0:0:0:0:
|
||||
325,196,91874,2,0,P|365:210|436:184,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
|
||||
430,190,92096,2,0,B|418:110,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
313,19,92318,2,0,L|190:36,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
|
||||
201,34,92540,2,0,B|214:117,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
209,252,92762,5,8,3:0:0:0:
|
||||
156,261,92874,1,0,0:0:0:0:
|
||||
112,231,92985,1,4,0:0:0:0:
|
||||
60,222,93096,1,0,0:0:0:0:
|
||||
13,247,93207,38,0,P|4:288|19:328,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
173,186,93429,1,4,0:0:0:0:
|
||||
215,120,93540,1,0,0:0:0:0:
|
||||
162,49,93651,2,0,P|125:39|76:61,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
|
||||
234,138,93874,2,0,P|273:157|313:148,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
385,39,94096,5,0,3:0:0:0:
|
||||
337,286,94318,2,0,L|322:373,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
409,327,94540,2,0,P|418:277|280:230,1,224.999996423721,8|0,3:0|0:0,0:0:0:0:
|
||||
239,319,94985,2,0,P|218:357|173:373,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
34,344,95207,37,4,0:0:0:0:
|
||||
21,309,95318,5,0,0:0:0:0:
|
||||
14,265,95429,5,8,3:0:0:0:
|
||||
27,223,95540,5,0,0:0:0:0:
|
||||
54,179,95651,5,4,0:0:0:0:
|
||||
94,147,95762,5,0,0:0:0:0:
|
||||
160,126,95873,5,0,3:0:0:0:
|
||||
231,149,95984,5,0,0:0:0:0:
|
||||
273,221,96096,5,4,0:0:0:0:
|
||||
353,262,96207,5,0,0:0:0:0:
|
||||
446,234,96318,5,8,3:0:0:0:
|
||||
501,145,96429,5,0,0:0:0:0:
|
||||
450,36,96540,5,4,0:0:0:0:
|
||||
239,44,96762,5,0,3:0:0:0:
|
||||
192,103,96873,1,0,0:0:0:0:
|
||||
118,118,96984,1,4,0:0:0:0:
|
||||
44,100,97096,1,0,0:0:0:0:
|
||||
5,36,97207,1,8,3:0:0:0:
|
||||
17,19,97429,37,4,0:0:0:0:
|
||||
146,51,97540,1,0,0:0:0:0:
|
||||
29,122,97651,2,0,L|39:193,1,56.2499991059302,0|0,3:0|0:0,0:0:0:0:
|
||||
44,197,97874,6,0,P|100:231|176:201,1,112.500002503395,4|0,0:0|0:0,0:0:0:0:
|
||||
301,160,98096,38,0,P|329:140|382:137,1,84.3750018775463,8|0,3:0|0:0,0:0:0:0:
|
||||
398,147,98318,6,0,B|431:187|431:187|415:279,1,112.500002503395,4|0,0:0|0:0,0:0:0:0:
|
||||
265,371,98540,38,0,L|180:361,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
127,202,98762,2,0,L|141:113,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
193,260,98985,2,0,P|144:291|68:278,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
|
||||
91,290,99207,2,0,L|79:373,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
20,184,99429,6,0,B|4:141|4:141|27:66,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
|
||||
23,78,99651,2,0,L|109:91,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
271,74,99874,2,0,P|254:31|222:12,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
|
||||
186,180,100096,2,0,P|232:175|260:147,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
132,63,100318,37,0,3:0:0:0:
|
||||
253,157,100540,1,4,0:0:0:0:
|
||||
285,167,100651,1,0,0:0:0:0:
|
||||
357,129,100762,5,8,3:0:0:0:
|
||||
389,139,100873,1,0,0:0:0:0:
|
||||
422,148,100985,2,0,P|407:200|416:233,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
459,377,101207,38,0,P|472:333|459:295,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
398,242,101429,2,0,L|314:257,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
165,354,101651,2,0,P|116:332|211:264,1,224.999996423721,8|0,3:0|0:0,0:0:0:0:
|
||||
302,165,102096,6,0,L|292:89,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
392,91,102318,2,0,L|382:14,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
192,229,102540,38,0,L|212:136,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
|
||||
107,172,102762,2,0,L|127:79,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
314,332,102985,6,0,L|305:278,1,37.4999994039535,0|0,3:0|0:0,0:0:0:0:
|
||||
343,345,103096,2,0,L|334:291,1,37.4999994039535,0|0,0:0|0:0,0:0:0:0:
|
||||
370,358,103207,2,0,L|361:304,1,37.4999994039535,4|0,0:0|0:0,0:0:0:0:
|
||||
380,117,103429,38,0,L|374:75,3,37.4999994039535,8|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
|
||||
444,166,103651,2,0,P|417:188|346:191,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
392,2,103874,2,0,P|424:14|462:74,1,74.999998807907,4|0,3:0|0:0,0:0:0:0:
|
||||
271,129,104096,2,0,P|265:94|298:31,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
505,113,104318,5,8,3:0:0:0:
|
||||
269,217,104540,38,0,L|216:216,3,37.4999994039535,0|0|0|0,3:0|3:0|3:0|3:0,0:0:0:0:
|
||||
360,220,104762,1,0,3:0:0:0:
|
||||
296,384,104874,1,4,3:0:0:0:
|
||||
102,307,105096,5,0,0:0:0:0:
|
||||
102,307,105207,2,0,B|206:381|258:244|374:330,1,269.999987468719,12|0,3:0|0:0,0:0:0:0:
|
||||
439,319,105651,6,0,P|379:336|396:236,1,168.750003755093,0|0,3:0|0:0,0:0:0:0:
|
||||
373,258,106096,6,0,P|374:315|443:283,1,112.500002503395,8|0,3:0|0:0,0:0:0:0:
|
||||
420,323,106651,37,0,0:0:0:0:
|
||||
469,245,106763,1,4,0:0:0:0:
|
||||
508,322,106874,1,0,0:0:0:0:
|
||||
379,245,106985,1,8,3:0:0:0:
|
||||
483,105,107207,6,0,L|474:40,3,56.2500012516975,4|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
|
||||
462,30,107429,38,0,P|401:56|319:25,1,149.999997615814,0|0,3:0|0:0,0:0:0:0:
|
||||
272,120,107874,2,0,P|184:91|118:125,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
|
||||
103,213,108207,2,0,B|128:232|128:232|269:200,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
|
||||
393,187,108540,2,0,L|385:286,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
|
||||
333,338,108763,1,8,3:0:0:0:
|
||||
467,307,108874,6,0,L|509:297,2,37.4999994039535,0|0|4,0:0|0:0|0:0,0:0:0:0:
|
||||
409,380,109096,1,0,0:0:0:0:
|
||||
300,257,109207,38,0,P|279:218|281:171,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
|
||||
401,118,109429,1,4,0:0:0:0:
|
||||
401,118,109651,6,0,L|315:109,1,74.999998807907,8|4,3:0|0:0,0:0:0:0:
|
||||
256,15,109985,37,0,0:0:0:0:
|
||||
175,121,110096,2,0,P|162:60|109:16,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
|
||||
128,26,110318,2,0,P|106:86|47:122,1,112.49999821186,0|0,0:0|0:0,0:0:0:0:
|
||||
69,114,110540,2,0,P|135:105|185:131,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
|
||||
160,223,110762,6,0,B|142:230|142:230|120:228|120:228|95:239|95:239|71:235|71:235|49:244|49:244|22:249,1,112.500002503395,4|0,0:0|0:0,0:0:0:0:
|
||||
193,334,110985,38,0,P|216:310|242:301,1,56.2500012516975,0|0,3:0|0:0,0:0:0:0:
|
||||
335,325,111207,2,0,P|366:353|378:379,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
|
||||
273,383,111429,2,0,L|304:213,1,168.750003755093,0|0,0:0|0:0,0:0:0:0:
|
||||
383,255,111874,22,0,B|422:273|422:273|476:273,1,74.999998807907,8|0,3:0|3:0,0:0:0:0:
|
||||
209,219,112096,2,0,B|169:221|169:221|131:206,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
|
||||
403,147,112318,2,0,B|352:114|352:114|337:43|337:43|295:109|295:109|234:115,1,269.999987468719,8|0,3:0|0:0,0:0:0:0:
|
||||
+1
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -19,23 +19,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
public class StackingTest
|
||||
{
|
||||
[Test]
|
||||
public void TestStacking()
|
||||
public void TestStackingEdgeCaseOne()
|
||||
{
|
||||
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(beatmap_data)))
|
||||
using (var reader = new LineBufferedReader(stream))
|
||||
{
|
||||
var beatmap = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
|
||||
var converted = new TestWorkingBeatmap(beatmap).GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>());
|
||||
|
||||
var objects = converted.HitObjects.ToList();
|
||||
|
||||
// The last hitobject triggers the stacking
|
||||
for (int i = 0; i < objects.Count - 1; i++)
|
||||
Assert.AreEqual(0, ((OsuHitObject)objects[i]).StackHeight);
|
||||
}
|
||||
}
|
||||
|
||||
private const string beatmap_data = @"
|
||||
using (var stream = new MemoryStream(@"
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
@@ -62,6 +48,65 @@ SliderTickRate:0.5
|
||||
311,185,218471,2,0,L|325:209,1,25
|
||||
311,185,218671,2,0,L|304:212,1,25
|
||||
311,185,240271,5,0,0:0:0:0:
|
||||
";
|
||||
"u8.ToArray()))
|
||||
using (var reader = new LineBufferedReader(stream))
|
||||
{
|
||||
var beatmap = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
|
||||
var converted = new TestWorkingBeatmap(beatmap).GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>());
|
||||
|
||||
var objects = converted.HitObjects.ToList();
|
||||
|
||||
// The last hitobject triggers the stacking
|
||||
for (int i = 0; i < objects.Count - 1; i++)
|
||||
ClassicAssert.AreEqual(0, ((OsuHitObject)objects[i]).StackHeight);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStackingEdgeCaseTwo()
|
||||
{
|
||||
using (var stream = new MemoryStream(@"
|
||||
osu file format v14
|
||||
// extracted from https://osu.ppy.sh/beatmapsets/365006#osu/801165
|
||||
|
||||
[General]
|
||||
StackLeniency: 0.2
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:6
|
||||
CircleSize:4
|
||||
OverallDifficulty:8
|
||||
ApproachRate:9.3
|
||||
SliderMultiplier:2
|
||||
SliderTickRate:1
|
||||
|
||||
[TimingPoints]
|
||||
5338,444.444444444444,4,2,0,50,1,0
|
||||
82893,-76.9230769230769,4,2,8,50,0,0
|
||||
85115,-76.9230769230769,4,2,0,50,0,0
|
||||
85337,-100,4,2,8,60,0,0
|
||||
85893,-100,4,2,7,60,0,0
|
||||
86226,-100,4,2,8,60,0,0
|
||||
88893,-58.8235294117647,4,1,8,70,0,1
|
||||
|
||||
[HitObjects]
|
||||
427,124,84226,1,0,3:0:0:0:
|
||||
427,124,84337,1,0,3:0:0:0:
|
||||
427,124,84449,1,8,0:0:0:0:
|
||||
"u8.ToArray()))
|
||||
using (var reader = new LineBufferedReader(stream))
|
||||
{
|
||||
var beatmap = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
|
||||
var converted = new TestWorkingBeatmap(beatmap).GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>());
|
||||
|
||||
var objects = converted.HitObjects.ToList();
|
||||
|
||||
Assert.That(objects, Has.Count.EqualTo(3));
|
||||
|
||||
// The last hitobject triggers the stacking
|
||||
for (int i = 0; i < objects.Count - 1; i++)
|
||||
ClassicAssert.AreEqual(0, ((OsuHitObject)objects[i]).StackHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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.Testing;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
[HeadlessTest]
|
||||
public partial class TestSceneAutoGeneration : OsuTestScene
|
||||
{
|
||||
[TestCase(-1, true)]
|
||||
[TestCase(0, false)]
|
||||
[TestCase(1, false)]
|
||||
public void TestAlternating(double offset, bool shouldAlternate)
|
||||
{
|
||||
const double first_object_time = 1000;
|
||||
double secondObjectTime = first_object_time + AutoGenerator.KEY_UP_DELAY + OsuAutoGenerator.MIN_FRAME_SEPARATION_FOR_ALTERNATING + offset;
|
||||
|
||||
var beatmap = new OsuBeatmap();
|
||||
beatmap.HitObjects.Add(new HitCircle { StartTime = first_object_time });
|
||||
beatmap.HitObjects.Add(new HitCircle { StartTime = secondObjectTime });
|
||||
|
||||
var generated = new OsuAutoGenerator(beatmap, []).Generate();
|
||||
var frames = generated.Frames.OfType<OsuReplayFrame>().ToList();
|
||||
|
||||
Assert.That(frames.Exists(f => f.Time == first_object_time && f.Actions.SingleOrDefault() == OsuAction.LeftButton));
|
||||
Assert.That(frames.Exists(f => f.Time == first_object_time + AutoGenerator.KEY_UP_DELAY && !f.Actions.Any()));
|
||||
|
||||
Assert.That(frames.Exists(f => f.Time == secondObjectTime && f.Actions.SingleOrDefault() == (shouldAlternate ? OsuAction.RightButton : OsuAction.LeftButton)));
|
||||
Assert.That(frames.Exists(f => f.Time == secondObjectTime + AutoGenerator.KEY_UP_DELAY && !f.Actions.Any()));
|
||||
}
|
||||
|
||||
[TestCase(300)]
|
||||
[TestCase(600)]
|
||||
[TestCase(1200)]
|
||||
public void TestAlternatingSpecificBPM(double bpm)
|
||||
{
|
||||
const double first_object_time = 1000;
|
||||
double secondObjectTime = first_object_time + 60000 / bpm;
|
||||
|
||||
var beatmap = new OsuBeatmap();
|
||||
beatmap.HitObjects.Add(new HitCircle { StartTime = first_object_time });
|
||||
beatmap.HitObjects.Add(new HitCircle { StartTime = secondObjectTime });
|
||||
|
||||
var generated = new OsuAutoGenerator(beatmap, []).Generate();
|
||||
var frames = generated.Frames.OfType<OsuReplayFrame>().ToList();
|
||||
|
||||
Assert.That(frames.Exists(f => f.Time == first_object_time && f.Actions.SingleOrDefault() == OsuAction.LeftButton));
|
||||
Assert.That(frames.Exists(f => f.Time == first_object_time + AutoGenerator.KEY_UP_DELAY && !f.Actions.Any()));
|
||||
|
||||
Assert.That(frames.Exists(f => f.Time == secondObjectTime && f.Actions.SingleOrDefault() == OsuAction.RightButton));
|
||||
Assert.That(frames.Exists(f => f.Time == secondObjectTime + AutoGenerator.KEY_UP_DELAY && !f.Actions.Any()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public partial class TestSceneGameplayCursorSizeChange : PlayerTestScene
|
||||
{
|
||||
private const float initial_cursor_size = 1f;
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
|
||||
[Resolved]
|
||||
private SkinManager? skins { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
if (skins != null) skins.CurrentSkinInfo.Value = skins.DefaultClassicSkin.SkinInfo;
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("Set gameplay cursor size: 1", () => LocalConfig.SetValue(OsuSetting.GameplayCursorSize, initial_cursor_size));
|
||||
AddStep("resume player", () => Player.GameplayClockContainer.Start());
|
||||
AddUntilStep("clock running", () => Player.GameplayClockContainer.IsRunning);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPausedChangeCursorSize()
|
||||
{
|
||||
AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
|
||||
AddStep("move cursor to top left", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft));
|
||||
AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
|
||||
AddStep("move cursor to top right", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopRight));
|
||||
AddStep("press escape", () => InputManager.Key(Key.Escape));
|
||||
|
||||
AddSliderStep("cursor size", 0.1f, 2f, 1f, v => LocalConfig.SetValue(OsuSetting.GameplayCursorSize, v));
|
||||
}
|
||||
|
||||
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, false);
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
Debug.Assert(drawableHitObject.HitObject.HitWindows != null);
|
||||
|
||||
double delay = drawableHitObject.HitObject.StartTime - (drawableHitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current;
|
||||
scheduledTasks.Add(Scheduler.AddDelayed(() => drawableHitObject.TriggerJudgement(), delay));
|
||||
scheduledTasks.Add(Scheduler.AddDelayed(drawableHitObject.TriggerJudgement, delay));
|
||||
|
||||
return drawableHitObject;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
@@ -10,6 +11,7 @@ using osu.Framework.Input.States;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Testing.Input;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
@@ -58,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
foreach (var smokeContainer in smokeContainers)
|
||||
{
|
||||
if (smokeContainer.Children.Count != 0)
|
||||
if (smokeContainer.Children.OfType<SkinnableDrawable>().Any())
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="NUnit" Version="4.5.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
continue;
|
||||
|
||||
double endTime = stackBaseObject.GetEndTime();
|
||||
double stackThreshold = objectN.TimePreempt * beatmap.StackLeniency;
|
||||
float stackThreshold = calculateStackThreshold(beatmap, objectN);
|
||||
|
||||
if (objectN.StartTime - endTime > stackThreshold)
|
||||
// We are no longer within stacking range of the next object.
|
||||
@@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
OsuHitObject objectI = hitObjects[i];
|
||||
if (objectI.StackHeight != 0 || objectI is Spinner) continue;
|
||||
|
||||
double stackThreshold = objectI.TimePreempt * beatmap.StackLeniency;
|
||||
float stackThreshold = calculateStackThreshold(beatmap, objectI);
|
||||
|
||||
/* If this object is a hitcircle, then we enter this "special" case.
|
||||
* It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider.
|
||||
@@ -151,7 +151,10 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
|
||||
double endTime = objectN.GetEndTime();
|
||||
|
||||
if (objectI.StartTime - endTime > stackThreshold)
|
||||
// truncation to integer is required to match stable
|
||||
// compare https://github.com/peppy/osu-stable-reference/blob/08e3dafd525934cf48880b08e91c24ce4ad8b761/osu!/GameplayElements/HitObjectManager.cs#L1725
|
||||
// - both quantities being subtracted there are integers
|
||||
if ((int)objectI.StartTime - (int)endTime > stackThreshold)
|
||||
// We are no longer within stacking range of the previous object.
|
||||
break;
|
||||
|
||||
@@ -232,7 +235,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
|
||||
for (int j = i + 1; j < hitObjects.Count; j++)
|
||||
{
|
||||
double stackThreshold = hitObjects[i].TimePreempt * beatmap.StackLeniency;
|
||||
float stackThreshold = calculateStackThreshold(beatmap, hitObjects[i]);
|
||||
|
||||
if (hitObjects[j].StartTime - stackThreshold > startTime)
|
||||
break;
|
||||
@@ -264,5 +267,17 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Truncation of <see cref="OsuHitObject.TimePreempt"/> to <see cref="int"/>, as well as keeping the result as <see cref="float"/>, are both done
|
||||
/// <a href="https://github.com/peppy/osu-stable-reference/blob/08e3dafd525934cf48880b08e91c24ce4ad8b761/osu!/GameplayElements/HitObjectManager.cs#L1652">
|
||||
/// for the purposes of stable compatibility
|
||||
/// </a>.
|
||||
/// Note that for top-level objects <see cref="OsuHitObject.TimePreempt"/> is supposed to be integral anyway;
|
||||
/// see <see cref="OsuHitObject.ApplyDefaultsToSelf"/> using <see cref="IBeatmapDifficultyInfo.DifficultyRangeInt"/> when calculating it.
|
||||
/// Slider ticks and end circles are the exception to that, but they do not matter for stacking.
|
||||
/// </remarks>
|
||||
private static float calculateStackThreshold(IBeatmap beatmap, OsuHitObject hitObject)
|
||||
=> (int)hitObject.TimePreempt * beatmap.StackLeniency;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Aim
|
||||
{
|
||||
public static class AgilityEvaluator
|
||||
{
|
||||
private const double distance_cap = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.2; // 1.2 circles distance between centers
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the difficulty of fast aiming
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||
{
|
||||
if (current.BaseObject is Spinner)
|
||||
return 0;
|
||||
|
||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||
var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null;
|
||||
|
||||
double travelDistance = osuPrevObj?.LazyTravelDistance ?? 0;
|
||||
double distance = travelDistance + osuCurrObj.LazyJumpDistance;
|
||||
|
||||
double distanceScaled = Math.Min(distance, distance_cap) / distance_cap;
|
||||
|
||||
double agilityDifficulty = distanceScaled * 1000 / osuCurrObj.AdjustedDeltaTime;
|
||||
|
||||
agilityDifficulty *= Math.Pow(osuCurrObj.SmallCircleBonus, 1.5);
|
||||
|
||||
agilityDifficulty *= highBpmBonus(osuCurrObj.AdjustedDeltaTime);
|
||||
|
||||
return agilityDifficulty;
|
||||
}
|
||||
|
||||
private static double highBpmBonus(double ms) => 1 / (1 - Math.Pow(0.2, ms / 1000));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// 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.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Aim
|
||||
{
|
||||
public static class FlowAimEvaluator
|
||||
{
|
||||
private const double velocity_change_multiplier = 0.52;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates difficulty of "flow aim" - aiming pattern where player doesn't stop their cursor on every object and instead "flows" through them.
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliderTravelDistance)
|
||||
{
|
||||
if (current.BaseObject is Spinner || current.Index <= 1 || current.Previous(0).BaseObject is Spinner)
|
||||
return 0;
|
||||
|
||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||
var osuLastObj = (OsuDifficultyHitObject)current.Previous(0);
|
||||
var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1);
|
||||
|
||||
double currDistance = withSliderTravelDistance ? osuCurrObj.LazyJumpDistance : osuCurrObj.JumpDistance;
|
||||
double prevDistance = withSliderTravelDistance ? osuLastObj.LazyJumpDistance : osuLastObj.JumpDistance;
|
||||
|
||||
double currVelocity = currDistance / osuCurrObj.AdjustedDeltaTime;
|
||||
|
||||
if (osuLastObj.BaseObject is Slider && withSliderTravelDistance)
|
||||
{
|
||||
// If the last object is a slider, then we extend the travel velocity through the slider into the current object.
|
||||
double sliderDistance = osuLastObj.LazyTravelDistance + osuCurrObj.LazyJumpDistance;
|
||||
currVelocity = Math.Max(currVelocity, sliderDistance / osuCurrObj.AdjustedDeltaTime);
|
||||
}
|
||||
|
||||
double prevVelocity = prevDistance / osuLastObj.AdjustedDeltaTime;
|
||||
|
||||
double flowDifficulty = currVelocity;
|
||||
|
||||
// Apply high circle size bonus to the base velocity.
|
||||
// We use reduced CS bonus here because the bonus was made for an evaluator with a different d/t scaling
|
||||
flowDifficulty *= Math.Sqrt(osuCurrObj.SmallCircleBonus);
|
||||
|
||||
// Rhythm changes are harder to flow
|
||||
flowDifficulty *= 1 + Math.Min(0.25,
|
||||
Math.Pow((Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) - Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime)) / 50, 4));
|
||||
|
||||
if (osuCurrObj.Angle != null && osuLastObj.Angle != null)
|
||||
{
|
||||
double angleDifference = Math.Abs(osuCurrObj.Angle.Value - osuLastObj.Angle.Value);
|
||||
double angleDifferenceAdjusted = Math.Sin(angleDifference / 2) * 180.0;
|
||||
double angularVelocity = angleDifferenceAdjusted / (osuCurrObj.AdjustedDeltaTime * 0.1);
|
||||
|
||||
// Low angular velocity flow (angles are consistent) is easier to follow than erratic flow
|
||||
flowDifficulty *= 0.8 + Math.Sqrt(angularVelocity / 270.0);
|
||||
}
|
||||
|
||||
// If all three notes are overlapping - don't reward bonuses as you don't have to do additional movement
|
||||
double overlappedNotesWeight = 1;
|
||||
|
||||
if (current.Index > 2)
|
||||
{
|
||||
double o1 = calculateOverlapFactor(osuCurrObj, osuLastObj);
|
||||
double o2 = calculateOverlapFactor(osuCurrObj, osuLastLastObj);
|
||||
double o3 = calculateOverlapFactor(osuLastObj, osuLastLastObj);
|
||||
|
||||
overlappedNotesWeight = 1 - o1 * o2 * o3;
|
||||
}
|
||||
|
||||
if (osuCurrObj.Angle != null)
|
||||
{
|
||||
// Acute angles are also hard to flow
|
||||
flowDifficulty += currVelocity *
|
||||
SnapAimEvaluator.CalcAngleAcuteness(osuCurrObj.Angle.Value) *
|
||||
overlappedNotesWeight;
|
||||
}
|
||||
|
||||
if (Math.Max(prevVelocity, currVelocity) != 0)
|
||||
{
|
||||
if (withSliderTravelDistance)
|
||||
{
|
||||
currVelocity = currDistance / osuCurrObj.AdjustedDeltaTime;
|
||||
}
|
||||
|
||||
// Scale with ratio of difference compared to 0.5 * max dist.
|
||||
double distRatio = DifficultyCalculationUtils.Smoothstep(Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity), 0, 1);
|
||||
|
||||
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
||||
double overlapVelocityBuff = Math.Min(OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25 / Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime),
|
||||
Math.Abs(prevVelocity - currVelocity));
|
||||
|
||||
flowDifficulty += overlapVelocityBuff *
|
||||
distRatio *
|
||||
overlappedNotesWeight *
|
||||
velocity_change_multiplier;
|
||||
}
|
||||
|
||||
if (osuCurrObj.BaseObject is Slider && withSliderTravelDistance)
|
||||
{
|
||||
// Include slider velocity to make velocity more consistent with snap
|
||||
flowDifficulty += osuCurrObj.TravelDistance / osuCurrObj.TravelTime;
|
||||
}
|
||||
|
||||
// Final velocity is being raised to a power because flow difficulty scales harder with both high distance and time, and we want to account for that
|
||||
flowDifficulty = Math.Pow(flowDifficulty, 1.45);
|
||||
|
||||
// Reduce difficulty for low spacing since spacing below radius is always to be flowed
|
||||
return flowDifficulty * DifficultyCalculationUtils.Smootherstep(currDistance, 0, OsuDifficultyHitObject.NORMALISED_RADIUS);
|
||||
}
|
||||
|
||||
private static double calculateOverlapFactor(OsuDifficultyHitObject first, OsuDifficultyHitObject second)
|
||||
{
|
||||
var firstBase = (OsuHitObject)first.BaseObject;
|
||||
var secondBase = (OsuHitObject)second.BaseObject;
|
||||
double objectRadius = firstBase.Radius;
|
||||
|
||||
double distance = Vector2.Distance(firstBase.StackedPosition, secondBase.StackedPosition);
|
||||
return Math.Clamp(1 - Math.Pow(Math.Max(distance - objectRadius, 0) / objectRadius, 2), 0, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
// 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.Extensions.ObjectExtensions;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Aim
|
||||
{
|
||||
public static class SnapAimEvaluator
|
||||
{
|
||||
private const double wide_angle_multiplier = 9.67;
|
||||
private const double acute_angle_multiplier = 2.41;
|
||||
private const double slider_multiplier = 1.5;
|
||||
private const double velocity_change_multiplier = 0.9;
|
||||
private const double wiggle_multiplier = 1.02; // WARNING: Increasing this multiplier beyond 1.02 reduces difficulty as distance increases. Refer to the desmos link above the wiggle bonus calculation
|
||||
private const double maximum_repetition_nerf = 0.15;
|
||||
private const double maximum_vector_influence = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the difficulty of aiming the current object, based on:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>cursor velocity to the current object,</description></item>
|
||||
/// <item><description>angle difficulty,</description></item>
|
||||
/// <item><description>sharp velocity increases,</description></item>
|
||||
/// <item><description>and slider difficulty.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliderTravelDistance)
|
||||
{
|
||||
if (current.BaseObject is Spinner || current.Index <= 1 || current.Previous(0).BaseObject is Spinner)
|
||||
return 0;
|
||||
|
||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||
var osuLastObj = (OsuDifficultyHitObject)current.Previous(0);
|
||||
var osuLast2Obj = (OsuDifficultyHitObject)current.Previous(2);
|
||||
|
||||
const int radius = OsuDifficultyHitObject.NORMALISED_RADIUS;
|
||||
const int diameter = OsuDifficultyHitObject.NORMALISED_DIAMETER;
|
||||
|
||||
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
|
||||
double currDistance = withSliderTravelDistance ? osuCurrObj.LazyJumpDistance : osuCurrObj.JumpDistance;
|
||||
double currVelocity = currDistance / osuCurrObj.AdjustedDeltaTime;
|
||||
|
||||
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
|
||||
if (osuLastObj.BaseObject is Slider && withSliderTravelDistance)
|
||||
{
|
||||
double sliderDistance = osuLastObj.LazyTravelDistance + osuCurrObj.LazyJumpDistance;
|
||||
currVelocity = Math.Max(currVelocity, sliderDistance / osuCurrObj.AdjustedDeltaTime);
|
||||
}
|
||||
|
||||
double prevDistance = withSliderTravelDistance ? osuLastObj.LazyJumpDistance : osuLastObj.JumpDistance;
|
||||
double prevVelocity = prevDistance / osuLastObj.AdjustedDeltaTime;
|
||||
|
||||
double snapDifficulty = currVelocity; // Start difficulty with regular velocity.
|
||||
|
||||
// Penalize angle repetition.
|
||||
snapDifficulty *= vectorAngleRepetition(osuCurrObj, osuLastObj);
|
||||
|
||||
if (osuCurrObj.Angle != null && osuLastObj.Angle != null)
|
||||
{
|
||||
double currAngle = osuCurrObj.Angle.Value;
|
||||
double lastAngle = osuLastObj.Angle.Value;
|
||||
|
||||
// Rewarding angles, take the smaller velocity as base.
|
||||
double velocityInfluence = Math.Min(currVelocity, prevVelocity);
|
||||
|
||||
double acuteAngleBonus = 0;
|
||||
|
||||
if (Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) < 1.25 * Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime)) // If rhythms are the same.
|
||||
{
|
||||
acuteAngleBonus = CalcAngleAcuteness(currAngle);
|
||||
|
||||
// Penalize angle repetition. It is important to do it _before_ multiplying by anything because we compare raw acuteness here
|
||||
acuteAngleBonus *= 0.08 + 0.92 * (1 - Math.Min(acuteAngleBonus, Math.Pow(CalcAngleAcuteness(lastAngle), 3)));
|
||||
|
||||
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
||||
acuteAngleBonus *= velocityInfluence * DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.AdjustedDeltaTime, 2), 300, 400) *
|
||||
DifficultyCalculationUtils.Smootherstep(currDistance, 0, diameter * 2);
|
||||
}
|
||||
|
||||
double wideAngleBonus = calcAngleWideness(currAngle);
|
||||
|
||||
// Penalize angle repetition. It is important to do it _before_ multiplying by velocity because we compare raw wideness here
|
||||
wideAngleBonus *= 0.25 + 0.75 * (1 - Math.Min(wideAngleBonus, Math.Pow(calcAngleWideness(lastAngle), 3)));
|
||||
|
||||
// Rescaling velocity for the wide angle bonus
|
||||
const double wide_angle_time_scale = 1.45;
|
||||
double wideAngleCurrVelocity = currDistance / Math.Pow(osuCurrObj.AdjustedDeltaTime, wide_angle_time_scale);
|
||||
double wideAnglePrevVelocity = prevDistance / Math.Pow(osuLastObj.AdjustedDeltaTime, wide_angle_time_scale);
|
||||
|
||||
if (osuLastObj.BaseObject is Slider && withSliderTravelDistance)
|
||||
{
|
||||
double sliderDistance = osuLastObj.LazyTravelDistance + osuCurrObj.LazyJumpDistance;
|
||||
wideAngleCurrVelocity = Math.Max(wideAngleCurrVelocity, sliderDistance / Math.Pow(osuCurrObj.AdjustedDeltaTime, wide_angle_time_scale));
|
||||
}
|
||||
|
||||
wideAngleBonus *= Math.Min(wideAngleCurrVelocity, wideAnglePrevVelocity);
|
||||
|
||||
if (osuLast2Obj != null)
|
||||
{
|
||||
// If objects just go back and forth through a middle point - don't give as much wide bonus
|
||||
// Use Previous(2) and Previous(0) because angles calculation is done prevprev-prev-curr, so any object's angle's center point is always the previous object
|
||||
var lastBaseObject = (OsuHitObject)osuLastObj.BaseObject;
|
||||
var last2BaseObject = (OsuHitObject)osuLast2Obj.BaseObject;
|
||||
|
||||
float distance = (last2BaseObject.StackedPosition - lastBaseObject.StackedPosition).Length;
|
||||
|
||||
if (distance < 1)
|
||||
{
|
||||
wideAngleBonus *= 1 - 0.55 * (1 - distance);
|
||||
}
|
||||
}
|
||||
|
||||
// Add in acute angle bonus or wide angle bonus, whichever is larger.
|
||||
snapDifficulty += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier);
|
||||
|
||||
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
||||
// https://www.desmos.com/calculator/dp0v0nvowc
|
||||
double wiggleBonus = velocityInfluence
|
||||
* DifficultyCalculationUtils.Smootherstep(currDistance, radius, diameter)
|
||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(currDistance, diameter * 3, diameter), 1.8)
|
||||
* DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60))
|
||||
* DifficultyCalculationUtils.Smootherstep(prevDistance, radius, diameter)
|
||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(prevDistance, diameter * 3, diameter), 1.8)
|
||||
* DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60));
|
||||
|
||||
snapDifficulty += wiggleBonus * wiggle_multiplier;
|
||||
}
|
||||
|
||||
if (Math.Max(prevVelocity, currVelocity) != 0)
|
||||
{
|
||||
if (withSliderTravelDistance)
|
||||
{
|
||||
// We want to use just the object jump without slider velocity when awarding differences
|
||||
currVelocity = currDistance / osuCurrObj.AdjustedDeltaTime;
|
||||
}
|
||||
|
||||
// Scale with ratio of difference compared to 0.5 * max dist.
|
||||
double distRatio = DifficultyCalculationUtils.Smoothstep(Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity), 0, 1);
|
||||
|
||||
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
||||
double overlapVelocityBuff = Math.Min(diameter * 1.25 / Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), Math.Abs(prevVelocity - currVelocity));
|
||||
|
||||
double velocityChangeBonus = overlapVelocityBuff * distRatio;
|
||||
|
||||
// Penalize for rhythm changes.
|
||||
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) / Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), 2);
|
||||
|
||||
snapDifficulty += velocityChangeBonus * velocity_change_multiplier;
|
||||
}
|
||||
|
||||
// Reward sliders based on velocity.
|
||||
if (osuCurrObj.BaseObject is Slider && withSliderTravelDistance)
|
||||
{
|
||||
double sliderBonus = osuCurrObj.TravelDistance / osuCurrObj.TravelTime;
|
||||
snapDifficulty += (sliderBonus < 1 ? sliderBonus : Math.Pow(sliderBonus, 0.75)) * slider_multiplier;
|
||||
}
|
||||
|
||||
// Apply high circle size bonus
|
||||
snapDifficulty *= osuCurrObj.SmallCircleBonus;
|
||||
|
||||
snapDifficulty *= highBpmBonus(osuCurrObj.AdjustedDeltaTime);
|
||||
|
||||
return snapDifficulty;
|
||||
}
|
||||
|
||||
private static double highBpmBonus(double ms) => 1 / (1 - Math.Pow(0.03, Math.Pow(ms / 1000, 0.65)));
|
||||
|
||||
private static double vectorAngleRepetition(OsuDifficultyHitObject current, OsuDifficultyHitObject previous)
|
||||
{
|
||||
if (current.Angle == null || previous.Angle == null)
|
||||
return 1;
|
||||
|
||||
const double note_limit = 6;
|
||||
|
||||
double constantAngleCount = 0;
|
||||
|
||||
for (int index = 0; index < note_limit; index++)
|
||||
{
|
||||
var loopObj = (OsuDifficultyHitObject)current.Previous(index);
|
||||
|
||||
if (loopObj.IsNull())
|
||||
break;
|
||||
|
||||
// Only consider vectors in the same jump section, stopping to change rhythm ruins momentum
|
||||
if (Math.Max(current.AdjustedDeltaTime, loopObj.AdjustedDeltaTime) > 1.1 * Math.Min(current.AdjustedDeltaTime, loopObj.AdjustedDeltaTime))
|
||||
break;
|
||||
|
||||
if (loopObj.NormalisedVectorAngle.IsNotNull() && current.NormalisedVectorAngle.IsNotNull())
|
||||
{
|
||||
double angleDifference = Math.Abs(current.NormalisedVectorAngle.Value - loopObj.NormalisedVectorAngle.Value);
|
||||
// Refer to this desmos for tuning, constants need to be precise so that values stay within the range of 0 and 1.
|
||||
// https://www.desmos.com/calculator/a8jesv5sv2
|
||||
constantAngleCount += Math.Cos(8 * Math.Min(double.DegreesToRadians(11.25), angleDifference));
|
||||
}
|
||||
}
|
||||
|
||||
double vectorRepetition = Math.Pow(Math.Min(0.5 / constantAngleCount, 1), 2);
|
||||
|
||||
double stackFactor = DifficultyCalculationUtils.Smootherstep(current.LazyJumpDistance, 0, OsuDifficultyHitObject.NORMALISED_DIAMETER);
|
||||
|
||||
double currAngle = current.Angle.Value;
|
||||
double lastAngle = previous.Angle.Value;
|
||||
|
||||
double angleDifferenceAdjusted = Math.Cos(2 * Math.Min(double.DegreesToRadians(45), Math.Abs(currAngle - lastAngle) * stackFactor));
|
||||
|
||||
double baseNerf = 1 - maximum_repetition_nerf * CalcAngleAcuteness(lastAngle) * angleDifferenceAdjusted;
|
||||
|
||||
return Math.Pow(baseNerf + (1 - baseNerf) * vectorRepetition * maximum_vector_influence * stackFactor, 2);
|
||||
}
|
||||
|
||||
private static double calcAngleWideness(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(40), double.DegreesToRadians(140));
|
||||
|
||||
public static double CalcAngleAcuteness(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(140), double.DegreesToRadians(40));
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
{
|
||||
public static class AimEvaluator
|
||||
{
|
||||
private const double wide_angle_multiplier = 1.5;
|
||||
private const double acute_angle_multiplier = 2.55;
|
||||
private const double slider_multiplier = 1.35;
|
||||
private const double velocity_change_multiplier = 0.75;
|
||||
private const double wiggle_multiplier = 1.02;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the difficulty of aiming the current object, based on:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>cursor velocity to the current object,</description></item>
|
||||
/// <item><description>angle difficulty,</description></item>
|
||||
/// <item><description>sharp velocity increases,</description></item>
|
||||
/// <item><description>and slider difficulty.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliderTravelDistance)
|
||||
{
|
||||
if (current.BaseObject is Spinner || current.Index <= 1 || current.Previous(0).BaseObject is Spinner)
|
||||
return 0;
|
||||
|
||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||
var osuLastObj = (OsuDifficultyHitObject)current.Previous(0);
|
||||
var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1);
|
||||
var osuLast2Obj = (OsuDifficultyHitObject)current.Previous(2);
|
||||
|
||||
const int radius = OsuDifficultyHitObject.NORMALISED_RADIUS;
|
||||
const int diameter = OsuDifficultyHitObject.NORMALISED_DIAMETER;
|
||||
|
||||
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
|
||||
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.AdjustedDeltaTime;
|
||||
|
||||
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
|
||||
if (osuLastObj.BaseObject is Slider && withSliderTravelDistance)
|
||||
{
|
||||
double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; // calculate the slider velocity from slider head to slider end.
|
||||
double movementVelocity = osuCurrObj.MinimumJumpDistance / osuCurrObj.MinimumJumpTime; // calculate the movement velocity from slider end to current object
|
||||
|
||||
currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity.
|
||||
}
|
||||
|
||||
// As above, do the same for the previous hitobject.
|
||||
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.AdjustedDeltaTime;
|
||||
|
||||
if (osuLastLastObj.BaseObject is Slider && withSliderTravelDistance)
|
||||
{
|
||||
double travelVelocity = osuLastLastObj.TravelDistance / osuLastLastObj.TravelTime;
|
||||
double movementVelocity = osuLastObj.MinimumJumpDistance / osuLastObj.MinimumJumpTime;
|
||||
|
||||
prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity);
|
||||
}
|
||||
|
||||
double wideAngleBonus = 0;
|
||||
double acuteAngleBonus = 0;
|
||||
double sliderBonus = 0;
|
||||
double velocityChangeBonus = 0;
|
||||
double wiggleBonus = 0;
|
||||
|
||||
double aimStrain = currVelocity; // Start strain with regular velocity.
|
||||
|
||||
if (osuCurrObj.Angle != null && osuLastObj.Angle != null)
|
||||
{
|
||||
double currAngle = osuCurrObj.Angle.Value;
|
||||
double lastAngle = osuLastObj.Angle.Value;
|
||||
|
||||
// Rewarding angles, take the smaller velocity as base.
|
||||
double angleBonus = Math.Min(currVelocity, prevVelocity);
|
||||
|
||||
if (Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) < 1.25 * Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime)) // If rhythms are the same.
|
||||
{
|
||||
acuteAngleBonus = calcAcuteAngleBonus(currAngle);
|
||||
|
||||
// Penalize angle repetition.
|
||||
acuteAngleBonus *= 0.08 + 0.92 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
|
||||
|
||||
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
||||
acuteAngleBonus *= angleBonus *
|
||||
DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.AdjustedDeltaTime, 2), 300, 400) *
|
||||
DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2);
|
||||
}
|
||||
|
||||
wideAngleBonus = calcWideAngleBonus(currAngle);
|
||||
|
||||
// Penalize angle repetition.
|
||||
wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3));
|
||||
|
||||
// Apply full wide angle bonus for distance more than one diameter
|
||||
wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter);
|
||||
|
||||
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
||||
// https://www.desmos.com/calculator/dp0v0nvowc
|
||||
wiggleBonus = angleBonus
|
||||
* DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter)
|
||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
||||
* DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60))
|
||||
* DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter)
|
||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
||||
* DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60));
|
||||
|
||||
if (osuLast2Obj != null)
|
||||
{
|
||||
// If objects just go back and forth through a middle point - don't give as much wide bonus
|
||||
// Use Previous(2) and Previous(0) because angles calculation is done prevprev-prev-curr, so any object's angle's center point is always the previous object
|
||||
var lastBaseObject = (OsuHitObject)osuLastObj.BaseObject;
|
||||
var last2BaseObject = (OsuHitObject)osuLast2Obj.BaseObject;
|
||||
|
||||
float distance = (last2BaseObject.StackedPosition - lastBaseObject.StackedPosition).Length;
|
||||
|
||||
if (distance < 1)
|
||||
{
|
||||
wideAngleBonus *= 1 - 0.35 * (1 - distance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Math.Max(prevVelocity, currVelocity) != 0)
|
||||
{
|
||||
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
|
||||
prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.AdjustedDeltaTime;
|
||||
currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.AdjustedDeltaTime;
|
||||
|
||||
// Scale with ratio of difference compared to 0.5 * max dist.
|
||||
double distRatio = DifficultyCalculationUtils.Smoothstep(Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity), 0, 1);
|
||||
|
||||
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
||||
double overlapVelocityBuff = Math.Min(diameter * 1.25 / Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), Math.Abs(prevVelocity - currVelocity));
|
||||
|
||||
velocityChangeBonus = overlapVelocityBuff * distRatio;
|
||||
|
||||
// Penalize for rhythm changes.
|
||||
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) / Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), 2);
|
||||
}
|
||||
|
||||
if (osuLastObj.BaseObject is Slider)
|
||||
{
|
||||
// Reward sliders based on velocity.
|
||||
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;
|
||||
}
|
||||
|
||||
aimStrain += wiggleBonus * wiggle_multiplier;
|
||||
aimStrain += velocityChangeBonus * velocity_change_multiplier;
|
||||
|
||||
// Add in acute angle bonus or wide angle bonus, whichever is larger.
|
||||
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier);
|
||||
|
||||
// Apply high circle size bonus
|
||||
aimStrain *= osuCurrObj.SmallCircleBonus;
|
||||
|
||||
// Add in additional slider velocity bonus.
|
||||
if (withSliderTravelDistance)
|
||||
aimStrain += sliderBonus * slider_multiplier;
|
||||
|
||||
return aimStrain;
|
||||
}
|
||||
|
||||
private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(40), double.DegreesToRadians(140));
|
||||
|
||||
private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(140), double.DegreesToRadians(40));
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
@@ -28,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
/// <item><description>and whether the hidden mod is enabled.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool hidden)
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
if (current.BaseObject is Spinner)
|
||||
return 0;
|
||||
@@ -40,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
double smallDistNerf = 1.0;
|
||||
double cumulativeStrainTime = 0.0;
|
||||
|
||||
double result = 0.0;
|
||||
double flashlightDifficulty = 0.0;
|
||||
|
||||
OsuDifficultyHitObject lastObj = osuCurrent;
|
||||
|
||||
@@ -66,9 +70,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
double stackNerf = Math.Min(1.0, (currentObj.LazyJumpDistance / scalingFactor) / 25.0);
|
||||
|
||||
// Bonus based on how visible the object is.
|
||||
double opacityBonus = 1.0 + max_opacity_bonus * (1.0 - osuCurrent.OpacityAt(currentHitObject.StartTime, hidden));
|
||||
double opacityBonus = 1.0 + max_opacity_bonus * (1.0 - osuCurrent.OpacityAt(currentHitObject.StartTime, mods.OfType<OsuModHidden>().Any(m => !m.OnlyFadeApproachCircles.Value)));
|
||||
|
||||
result += stackNerf * opacityBonus * scalingFactor * jumpDistance / cumulativeStrainTime;
|
||||
flashlightDifficulty += stackNerf * opacityBonus * scalingFactor * jumpDistance / cumulativeStrainTime;
|
||||
|
||||
if (currentObj.Angle != null && osuCurrent.Angle != null)
|
||||
{
|
||||
@@ -81,14 +85,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
lastObj = currentObj;
|
||||
}
|
||||
|
||||
result = Math.Pow(smallDistNerf * result, 2.0);
|
||||
flashlightDifficulty = Math.Pow(smallDistNerf * flashlightDifficulty, 2.0);
|
||||
|
||||
// Additional bonus for Hidden due to there being no approach circles.
|
||||
if (hidden)
|
||||
result *= 1.0 + hidden_bonus;
|
||||
if (mods.OfType<OsuModHidden>().Any())
|
||||
flashlightDifficulty *= 1.0 + hidden_bonus;
|
||||
|
||||
// Nerf patterns with repeated angles.
|
||||
result *= min_angle_multiplier + (1.0 - min_angle_multiplier) / (angleRepeatCount + 1.0);
|
||||
flashlightDifficulty *= min_angle_multiplier + (1.0 - min_angle_multiplier) / (angleRepeatCount + 1.0);
|
||||
|
||||
double sliderBonus = 0.0;
|
||||
|
||||
@@ -108,9 +112,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
sliderBonus /= (osuSlider.RepeatCount + 1);
|
||||
}
|
||||
|
||||
result += sliderBonus * slider_multiplier;
|
||||
flashlightDifficulty += sliderBonus * slider_multiplier;
|
||||
|
||||
return result;
|
||||
return flashlightDifficulty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
{
|
||||
public static class ReadingEvaluator
|
||||
{
|
||||
private const double reading_window_size = 3000; // 3 seconds
|
||||
private const double distance_influence_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.5; // 1.5 circles distance between centers
|
||||
private const double hidden_multiplier = 0.28;
|
||||
private const double density_multiplier = 2.4;
|
||||
private const double density_difficulty_base = 2.5;
|
||||
private const double preempt_balancing_factor = 140000;
|
||||
private const double preempt_starting_point = 500; // AR 9.66 in milliseconds
|
||||
private const double minimum_angle_relevancy_time = 2000; // 2 seconds
|
||||
private const double maximum_angle_relevancy_time = 200;
|
||||
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool hidden)
|
||||
{
|
||||
if (current.BaseObject is Spinner || current.Index == 0)
|
||||
return 0;
|
||||
|
||||
var currObj = (OsuDifficultyHitObject)current;
|
||||
var nextObj = (OsuDifficultyHitObject)current.Next(0);
|
||||
|
||||
double velocity = Math.Max(1, currObj.LazyJumpDistance / currObj.AdjustedDeltaTime); // Only allow velocity to buff
|
||||
|
||||
double currentVisibleObjectDensity = retrieveCurrentVisibleObjectDensity(currObj);
|
||||
double pastObjectDifficultyInfluence = getPastObjectDifficultyInfluence(currObj);
|
||||
|
||||
double constantAngleNerfFactor = getConstantAngleNerfFactor(currObj);
|
||||
|
||||
double noteDensityDifficulty = calculateDensityDifficulty(nextObj, velocity, constantAngleNerfFactor, pastObjectDifficultyInfluence, currentVisibleObjectDensity);
|
||||
|
||||
double hiddenDifficulty = hidden
|
||||
? calculateHiddenDifficulty(currObj, pastObjectDifficultyInfluence, currentVisibleObjectDensity, velocity, constantAngleNerfFactor)
|
||||
: 0;
|
||||
|
||||
double preemptDifficulty = calculatePreemptDifficulty(velocity, constantAngleNerfFactor, currObj.Preempt);
|
||||
|
||||
double readingDifficulty = DifficultyCalculationUtils.Norm(1.5, preemptDifficulty, hiddenDifficulty, noteDensityDifficulty);
|
||||
|
||||
// Having less time to process information is harder
|
||||
readingDifficulty *= highBpmBonus(currObj.AdjustedDeltaTime);
|
||||
|
||||
return readingDifficulty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the density difficulty of the current object and how hard it is to aim it because of it based on:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>cursor velocity to the current object,</description></item>
|
||||
/// <item><description>how many times the current object's angle was repeated,</description></item>
|
||||
/// <item><description>density of objects visible when the current object appears,</description></item>
|
||||
/// <item><description>density of objects visible when the current object needs to be clicked,</description></item>
|
||||
/// /// </list>
|
||||
/// </summary>
|
||||
private static double calculateDensityDifficulty(OsuDifficultyHitObject? nextObj, double velocity, double constantAngleNerfFactor,
|
||||
double pastObjectDifficultyInfluence, double currentVisibleObjectDensity)
|
||||
{
|
||||
// Consider future densities too because it can make the path the cursor takes less clear
|
||||
double futureObjectDifficultyInfluence = Math.Sqrt(currentVisibleObjectDensity);
|
||||
|
||||
if (nextObj != null)
|
||||
{
|
||||
// Reduce difficulty if movement to next object is small
|
||||
futureObjectDifficultyInfluence *= DifficultyCalculationUtils.Smootherstep(nextObj.LazyJumpDistance, 15, distance_influence_threshold);
|
||||
}
|
||||
|
||||
// Value higher note densities exponentially
|
||||
double noteDensityDifficulty = Math.Pow(pastObjectDifficultyInfluence + futureObjectDifficultyInfluence, 1.7) * 0.4 * constantAngleNerfFactor * velocity;
|
||||
|
||||
// Award only denser than average maps.
|
||||
noteDensityDifficulty = Math.Max(0, noteDensityDifficulty - density_difficulty_base);
|
||||
|
||||
// Apply a soft cap to general density reading to account for partial memorization
|
||||
noteDensityDifficulty = Math.Pow(noteDensityDifficulty, 0.45) * density_multiplier;
|
||||
|
||||
return noteDensityDifficulty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the difficulty of aiming the current object when the approach rate is very high based on:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>cursor velocity to the current object,</description></item>
|
||||
/// <item><description>how many times the current object's angle was repeated,</description></item>
|
||||
/// <item><description>how many milliseconds elapse between the approach circle appearing and touching the inner circle</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
private static double calculatePreemptDifficulty(double velocity, double constantAngleNerfFactor, double preempt)
|
||||
{
|
||||
// Arbitrary curve for the base value preempt difficulty should have as approach rate increases.
|
||||
// https://www.desmos.com/calculator/c175335a71
|
||||
double preemptDifficulty = Math.Pow((preempt_starting_point - preempt + Math.Abs(preempt - preempt_starting_point)) / 2, 2.5) / preempt_balancing_factor;
|
||||
|
||||
preemptDifficulty *= constantAngleNerfFactor * velocity;
|
||||
|
||||
return preemptDifficulty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the difficulty of aiming the current object when the hidden mod is active based on:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>cursor velocity to the current object,</description></item>
|
||||
/// <item><description>time the current object spends invisible,</description></item>
|
||||
/// <item><description>density of objects visible when the current object appears,</description></item>
|
||||
/// <item><description>density of objects visible when the current object needs to be clicked,</description></item>
|
||||
/// <item><description>how many times the current object's angle was repeated,</description></item>
|
||||
/// <item><description>if the current object is perfectly stacked to the previous one</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
private static double calculateHiddenDifficulty(OsuDifficultyHitObject currObj, double pastObjectDifficultyInfluence, double currentVisibleObjectDensity, double velocity,
|
||||
double constantAngleNerfFactor)
|
||||
{
|
||||
// Higher preempt means that time spent invisible is higher too, we want to reward that
|
||||
double preemptFactor = Math.Pow(currObj.Preempt, 2.2) * 0.01;
|
||||
|
||||
// Account for both past and current densities
|
||||
double densityFactor = Math.Pow(currentVisibleObjectDensity + pastObjectDifficultyInfluence, 3.3) * 3;
|
||||
|
||||
double hiddenDifficulty = (preemptFactor + densityFactor) * constantAngleNerfFactor * velocity * 0.01;
|
||||
|
||||
// Apply a soft cap to general HD reading to account for partial memorization
|
||||
hiddenDifficulty = Math.Pow(hiddenDifficulty, 0.4) * hidden_multiplier;
|
||||
|
||||
var previousObj = (OsuDifficultyHitObject)currObj.Previous(0);
|
||||
|
||||
// Buff perfect stacks only if current note is completely invisible at the time you click the previous note.
|
||||
if (currObj.LazyJumpDistance == 0 && currObj.OpacityAt(previousObj.BaseObject.StartTime, true) == 0 && previousObj.StartTime > currObj.StartTime - currObj.Preempt)
|
||||
hiddenDifficulty += hidden_multiplier * 2500 / Math.Pow(currObj.AdjustedDeltaTime, 1.5); // Perfect stacks are harder the less time between notes
|
||||
|
||||
return hiddenDifficulty;
|
||||
}
|
||||
|
||||
private static double getPastObjectDifficultyInfluence(OsuDifficultyHitObject currObj)
|
||||
{
|
||||
double pastObjectDifficultyInfluence = 0;
|
||||
|
||||
foreach (var loopObj in retrievePastVisibleObjects(currObj))
|
||||
{
|
||||
double loopDifficulty = currObj.OpacityAt(loopObj.BaseObject.StartTime, false);
|
||||
|
||||
// When aiming an object small distances mean previous objects may be cheesed, so it doesn't matter whether they were arranged confusingly.
|
||||
loopDifficulty *= DifficultyCalculationUtils.Smootherstep(loopObj.LazyJumpDistance, 15, distance_influence_threshold);
|
||||
|
||||
// Account less for objects close to the max reading window
|
||||
double timeBetweenCurrAndLoopObj = currObj.StartTime - loopObj.StartTime;
|
||||
double timeNerfFactor = getTimeNerfFactor(timeBetweenCurrAndLoopObj);
|
||||
|
||||
loopDifficulty *= timeNerfFactor;
|
||||
pastObjectDifficultyInfluence += loopDifficulty;
|
||||
}
|
||||
|
||||
return pastObjectDifficultyInfluence;
|
||||
}
|
||||
|
||||
// Returns a list of objects that are visible on screen at the point in time the current object becomes visible.
|
||||
private static IEnumerable<OsuDifficultyHitObject> retrievePastVisibleObjects(OsuDifficultyHitObject current)
|
||||
{
|
||||
for (int i = 0; i < current.Index; i++)
|
||||
{
|
||||
OsuDifficultyHitObject hitObject = (OsuDifficultyHitObject)current.Previous(i);
|
||||
|
||||
if (hitObject.IsNull() ||
|
||||
current.StartTime - hitObject.StartTime > reading_window_size ||
|
||||
hitObject.StartTime < current.StartTime - current.Preempt) // Current object not visible at the time object needs to be clicked
|
||||
break;
|
||||
|
||||
yield return hitObject;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the density of objects visible at the point in time the current object needs to be clicked capped by the reading window.
|
||||
private static double retrieveCurrentVisibleObjectDensity(OsuDifficultyHitObject current)
|
||||
{
|
||||
double visibleObjectCount = 0;
|
||||
|
||||
OsuDifficultyHitObject? hitObject = (OsuDifficultyHitObject)current.Next(0);
|
||||
|
||||
while (hitObject != null)
|
||||
{
|
||||
if (hitObject.StartTime - current.StartTime > reading_window_size ||
|
||||
current.StartTime < hitObject.StartTime - hitObject.Preempt) // Object not visible at the time current object needs to be clicked.
|
||||
break;
|
||||
|
||||
double timeBetweenCurrAndLoopObj = hitObject.StartTime - current.StartTime;
|
||||
double timeNerfFactor = getTimeNerfFactor(timeBetweenCurrAndLoopObj);
|
||||
|
||||
visibleObjectCount += hitObject.OpacityAt(current.BaseObject.StartTime, false) * timeNerfFactor;
|
||||
|
||||
hitObject = (OsuDifficultyHitObject?)hitObject.Next(0);
|
||||
}
|
||||
|
||||
return visibleObjectCount;
|
||||
}
|
||||
|
||||
// Returns a factor of how often the current object's angle has been repeated in a certain time frame.
|
||||
// It does this by checking the difference in angle between current and past objects and sums them based on a range of similarity.
|
||||
// https://www.desmos.com/calculator/eb057a4822
|
||||
private static double getConstantAngleNerfFactor(OsuDifficultyHitObject current)
|
||||
{
|
||||
double constantAngleCount = 0;
|
||||
int index = 0;
|
||||
double currentTimeGap = 0;
|
||||
|
||||
OsuDifficultyHitObject loopObjPrev0 = current;
|
||||
OsuDifficultyHitObject? loopObjPrev1 = null;
|
||||
OsuDifficultyHitObject? loopObjPrev2 = null;
|
||||
|
||||
while (currentTimeGap < minimum_angle_relevancy_time)
|
||||
{
|
||||
var loopObj = (OsuDifficultyHitObject)current.Previous(index);
|
||||
|
||||
if (loopObj.IsNull())
|
||||
break;
|
||||
|
||||
// Account less for objects that are close to the time limit.
|
||||
double longIntervalFactor = 1 - DifficultyCalculationUtils.ReverseLerp(loopObj.AdjustedDeltaTime, maximum_angle_relevancy_time, minimum_angle_relevancy_time);
|
||||
|
||||
if (loopObj.Angle.IsNotNull() && current.Angle.IsNotNull())
|
||||
{
|
||||
double angleDifference = Math.Abs(current.Angle.Value - loopObj.Angle.Value);
|
||||
double angleDifferenceAlternating = Math.PI;
|
||||
|
||||
if (loopObjPrev0.Angle != null && loopObjPrev1?.Angle != null && loopObjPrev2?.Angle != null)
|
||||
{
|
||||
angleDifferenceAlternating = Math.Abs(loopObjPrev1.Angle.Value - loopObj.Angle.Value);
|
||||
angleDifferenceAlternating += Math.Abs(loopObjPrev2.Angle.Value - loopObjPrev0.Angle.Value);
|
||||
|
||||
double weight = 1.0;
|
||||
|
||||
// Be sure that one of the angles is very sharp, when other is wide
|
||||
weight *= DifficultyCalculationUtils.ReverseLerp(Math.Min(loopObj.Angle.Value, loopObjPrev0.Angle.Value) * 180 / Math.PI, 20, 5);
|
||||
weight *= DifficultyCalculationUtils.ReverseLerp(Math.Max(loopObj.Angle.Value, loopObjPrev0.Angle.Value) * 180 / Math.PI, 60, 120);
|
||||
|
||||
// Lerp between max angle difference and rescaled alternating difference, with more harsh scaling compared to normal difference
|
||||
angleDifferenceAlternating = double.Lerp(Math.PI, 0.1 * angleDifferenceAlternating, weight);
|
||||
}
|
||||
|
||||
double stackFactor = DifficultyCalculationUtils.Smootherstep(loopObj.LazyJumpDistance, 0, OsuDifficultyHitObject.NORMALISED_RADIUS);
|
||||
|
||||
constantAngleCount += Math.Cos(3 * Math.Min(double.DegreesToRadians(30), Math.Min(angleDifference, angleDifferenceAlternating) * stackFactor)) * longIntervalFactor;
|
||||
}
|
||||
|
||||
currentTimeGap = current.StartTime - loopObj.StartTime;
|
||||
index++;
|
||||
|
||||
loopObjPrev2 = loopObjPrev1;
|
||||
loopObjPrev1 = loopObjPrev0;
|
||||
loopObjPrev0 = loopObj;
|
||||
}
|
||||
|
||||
return Math.Clamp(2 / constantAngleCount, 0.2, 1);
|
||||
}
|
||||
|
||||
// Returns a nerfing factor for when objects are very distant in time, affecting reading less.
|
||||
private static double getTimeNerfFactor(double deltaTime)
|
||||
{
|
||||
return Math.Clamp(2 - deltaTime / (reading_window_size / 2), 0, 1);
|
||||
}
|
||||
|
||||
private static double highBpmBonus(double ms) => 1 / (1 - Math.Pow(0.8, ms / 1000));
|
||||
}
|
||||
}
|
||||
+62
-32
@@ -8,15 +8,16 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed
|
||||
{
|
||||
public static class RhythmEvaluator
|
||||
{
|
||||
private const int history_time_max = 5 * 1000; // 5 seconds
|
||||
private const int history_objects_max = 32;
|
||||
private const double rhythm_overall_multiplier = 1.0;
|
||||
private const double rhythm_ratio_multiplier = 15.0;
|
||||
private const double rhythm_overall_multiplier = 0.95;
|
||||
private const double rhythm_ratio_multiplier = 26.0;
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
|
||||
@@ -26,11 +27,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
if (current.BaseObject is Spinner)
|
||||
return 0;
|
||||
|
||||
var currentOsuObject = (OsuDifficultyHitObject)current;
|
||||
|
||||
double rhythmComplexitySum = 0;
|
||||
|
||||
double deltaDifferenceEpsilon = ((OsuDifficultyHitObject)current).HitWindowGreat * 0.3;
|
||||
double deltaDifferenceEpsilon = ((OsuDifficultyHitObject)current).HitWindow(HitResult.Great) * 0.3;
|
||||
|
||||
var island = new Island(deltaDifferenceEpsilon);
|
||||
var previousIsland = new Island(deltaDifferenceEpsilon);
|
||||
@@ -57,6 +56,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
for (int i = rhythmStart; i > 0; i--)
|
||||
{
|
||||
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1);
|
||||
if (currObj.BaseObject is Spinner)
|
||||
continue;
|
||||
|
||||
// scales note 0 to 1 from history to now
|
||||
double timeDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max;
|
||||
@@ -64,44 +65,56 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
|
||||
double currHistoricalDecay = Math.Min(noteDecay, timeDecay); // either we're limited by time or limited by object count.
|
||||
|
||||
// Use custom cap value to ensure that that at this point delta time is actually zero
|
||||
// Use custom cap value to ensure that at this point delta time is actually zero
|
||||
double currDelta = Math.Max(currObj.DeltaTime, 1e-7);
|
||||
double prevDelta = Math.Max(prevObj.DeltaTime, 1e-7);
|
||||
double lastDelta = Math.Max(lastObj.DeltaTime, 1e-7);
|
||||
|
||||
// Make sure to always have the current island initialised - if we don't do it here it will only initialise on the next rhythm change
|
||||
if (island.Delta == int.MaxValue)
|
||||
island = new Island((int)currDelta, deltaDifferenceEpsilon);
|
||||
|
||||
// calculate how much current delta difference deserves a rhythm bonus
|
||||
// this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200)
|
||||
double deltaDifference = Math.Max(prevDelta, currDelta) / Math.Min(prevDelta, currDelta);
|
||||
|
||||
// Take only the fractional part of the value since we're only interested in punishing multiples
|
||||
double deltaDifferenceFraction = deltaDifference - Math.Truncate(deltaDifference);
|
||||
|
||||
double currRatio = 1.0 + rhythm_ratio_multiplier * Math.Min(0.5, DifficultyCalculationUtils.SmoothstepBellCurve(deltaDifferenceFraction));
|
||||
|
||||
// reduce ratio bonus if delta difference is too big
|
||||
double differenceMultiplier = Math.Clamp(2.0 - deltaDifference / 8.0, 0.0, 1.0);
|
||||
|
||||
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon);
|
||||
|
||||
double effectiveRatio = windowPenalty * currRatio * differenceMultiplier;
|
||||
double effectiveRatio = getEffectiveRatio(deltaDifference) * windowPenalty * differenceMultiplier;
|
||||
|
||||
// if previous object is a slider it might be easier to tap since you don't have to do a whole tapping motion
|
||||
// while a full deltatime might end up some weird ratio the "unpress->tap" motion might be simple
|
||||
// for example a slider-circle-circle pattern should be evaluated as a regular triple and not as a single->double
|
||||
if (prevObj.BaseObject is Slider)
|
||||
{
|
||||
double sliderLazyEndDelta = currObj.MinimumJumpTime;
|
||||
double sliderLazyDeltaDifference = Math.Max(sliderLazyEndDelta, currDelta) / Math.Min(sliderLazyEndDelta, currDelta);
|
||||
|
||||
double sliderRealEndDelta = currObj.LastObjectEndDeltaTime;
|
||||
double sliderRealDeltaDifference = Math.Max(sliderRealEndDelta, currDelta) / Math.Min(sliderRealEndDelta, currDelta);
|
||||
|
||||
double sliderEffectiveRatio = Math.Min(getEffectiveRatio(sliderLazyDeltaDifference), getEffectiveRatio(sliderRealDeltaDifference));
|
||||
effectiveRatio = Math.Min(sliderEffectiveRatio, effectiveRatio);
|
||||
}
|
||||
|
||||
bool isSpeedingUp = prevDelta > currDelta + deltaDifferenceEpsilon;
|
||||
|
||||
if (Math.Abs(prevDelta - currDelta) < deltaDifferenceEpsilon)
|
||||
{
|
||||
// island is still progressing
|
||||
island.AddDelta((int)currDelta);
|
||||
}
|
||||
|
||||
if (firstDeltaSwitch)
|
||||
{
|
||||
if (Math.Abs(prevDelta - currDelta) < deltaDifferenceEpsilon)
|
||||
{
|
||||
// island is still progressing
|
||||
island.AddDelta((int)currDelta);
|
||||
}
|
||||
else
|
||||
if (Math.Abs(prevDelta - currDelta) > deltaDifferenceEpsilon)
|
||||
{
|
||||
// bpm change is into slider, this is easy acc window
|
||||
if (currObj.BaseObject is Slider)
|
||||
effectiveRatio *= 0.125;
|
||||
|
||||
// bpm change was from a slider, this is easier typically than circle -> circle
|
||||
// unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders
|
||||
if (prevObj.BaseObject is Slider)
|
||||
effectiveRatio *= 0.3;
|
||||
effectiveRatio *= 0.5;
|
||||
|
||||
// repeated island polarity (2 -> 4, 3 -> 5)
|
||||
if (island.IsSimilarPolarity(previousIsland))
|
||||
@@ -116,6 +129,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
if (previousIsland.DeltaCount == island.DeltaCount)
|
||||
effectiveRatio *= 0.5;
|
||||
|
||||
if (isSpeedingUp)
|
||||
effectiveRatio *= 0.65;
|
||||
|
||||
var islandCount = islandCounts.FirstOrDefault(x => x.Island.Equals(island));
|
||||
|
||||
if (islandCount != default)
|
||||
@@ -134,7 +150,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
}
|
||||
else
|
||||
{
|
||||
islandCounts.Add((island, 1));
|
||||
if (island.DeltaCount > 0)
|
||||
{
|
||||
islandCounts.Add((island, 1));
|
||||
}
|
||||
}
|
||||
|
||||
// scale down the difficulty if the object is doubletappable
|
||||
@@ -176,10 +195,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
prevObj = currObj;
|
||||
}
|
||||
|
||||
double rhythmDifficulty = Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
||||
rhythmDifficulty *= 1 - currentOsuObject.GetDoubletapness((OsuDifficultyHitObject)current.Next(0));
|
||||
// If the current island is long we don't want the sum to have as big of an effect
|
||||
rhythmComplexitySum *= DifficultyCalculationUtils.ReverseLerp(island.DeltaCount, 22, 3);
|
||||
|
||||
return rhythmDifficulty;
|
||||
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though);
|
||||
}
|
||||
|
||||
private static double getEffectiveRatio(double deltaDifference)
|
||||
{
|
||||
// Take only the fractional part of the value since we're only interested in punishing multiples
|
||||
double deltaDifferenceFraction = deltaDifference - Math.Truncate(deltaDifference);
|
||||
|
||||
return 1.0 + rhythm_ratio_multiplier * Math.Min(0.5, DifficultyCalculationUtils.SmoothstepBellCurve(deltaDifferenceFraction));
|
||||
}
|
||||
|
||||
private class Island : IEquatable<Island>
|
||||
@@ -211,9 +238,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
|
||||
public bool IsSimilarPolarity(Island other)
|
||||
{
|
||||
// TODO: consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple)
|
||||
// naively adding delta check here breaks _a lot_ of maps because of the flawed ratio calculation
|
||||
return DeltaCount % 2 == other.DeltaCount % 2;
|
||||
// single delta islands shouldn't be compared
|
||||
if (DeltaCount <= 1 || other.DeltaCount <= 1)
|
||||
return false;
|
||||
|
||||
return Math.Abs(Delta - other.Delta) < deltaDifferenceEpsilon &&
|
||||
DeltaCount % 2 == other.DeltaCount % 2;
|
||||
}
|
||||
|
||||
public bool Equals(Island? other)
|
||||
+10
-29
@@ -2,47 +2,39 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed
|
||||
{
|
||||
public static class SpeedEvaluator
|
||||
{
|
||||
private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
|
||||
private const double min_speed_bonus = 200; // 200 BPM 1/4th
|
||||
private const double speed_balancing_factor = 40;
|
||||
private const double distance_multiplier = 0.8;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the difficulty of tapping the current object, based on:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>time between pressing the previous and current object,</description></item>
|
||||
/// <item><description>distance between those objects,</description></item>
|
||||
/// <item><description>and how easily they can be cheesed.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, IReadOnlyList<Mod> mods)
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||
{
|
||||
if (current.BaseObject is Spinner)
|
||||
return 0;
|
||||
|
||||
// derive strainTime for calculation
|
||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||
var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null;
|
||||
|
||||
double strainTime = osuCurrObj.AdjustedDeltaTime;
|
||||
double doubletapness = 1.0 - osuCurrObj.GetDoubletapness((OsuDifficultyHitObject?)osuCurrObj.Next(0));
|
||||
|
||||
// Cap deltatime to the OD 300 hitwindow.
|
||||
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
||||
strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindowGreat) / 0.93, 0.92, 1);
|
||||
strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindow(HitResult.Great)) / 0.93, 0.92, 1);
|
||||
|
||||
// speedBonus will be 0.0 for BPM < 200
|
||||
double speedBonus = 0.0;
|
||||
@@ -51,26 +43,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
if (DifficultyCalculationUtils.MillisecondsToBPM(strainTime) > min_speed_bonus)
|
||||
speedBonus = 0.75 * Math.Pow((DifficultyCalculationUtils.BPMToMilliseconds(min_speed_bonus) - strainTime) / speed_balancing_factor, 2);
|
||||
|
||||
double travelDistance = osuPrevObj?.TravelDistance ?? 0;
|
||||
double distance = travelDistance + osuCurrObj.MinimumJumpDistance;
|
||||
|
||||
// Cap distance at single_spacing_threshold
|
||||
distance = Math.Min(distance, single_spacing_threshold);
|
||||
|
||||
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
|
||||
double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier;
|
||||
|
||||
// Apply reduced small circle bonus because flow aim difficulty on small circles doesn't scale as hard as jumps
|
||||
distanceBonus *= Math.Sqrt(osuCurrObj.SmallCircleBonus);
|
||||
|
||||
if (mods.OfType<OsuModAutopilot>().Any())
|
||||
distanceBonus = 0;
|
||||
|
||||
// Base difficulty with all bonuses
|
||||
double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime;
|
||||
double speedDifficulty = (1 + speedBonus) * 1000 / strainTime;
|
||||
|
||||
speedDifficulty *= highBpmBonus(osuCurrObj.AdjustedDeltaTime);
|
||||
|
||||
// Apply penalty if there's doubletappable doubles
|
||||
return difficulty * doubletapness;
|
||||
return speedDifficulty * doubletapness;
|
||||
}
|
||||
|
||||
private static double highBpmBonus(double ms) => 1 / (1 - Math.Pow(0.3, ms / 1000));
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("flashlight_difficulty")]
|
||||
public double FlashlightDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the reading skill.
|
||||
/// </summary>
|
||||
[JsonProperty("reading_difficulty")]
|
||||
public double ReadingDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Describes how much of <see cref="AimDifficulty"/> is contributed to by hitcircles or sliders.
|
||||
/// A value closer to 1.0 indicates most of <see cref="AimDifficulty"/> is contributed by hitcircles.
|
||||
@@ -75,6 +81,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("speed_difficult_strain_count")]
|
||||
public double SpeedDifficultStrainCount { get; set; }
|
||||
|
||||
[JsonProperty("reading_difficult_note_count")]
|
||||
public double ReadingDifficultNoteCount { get; set; }
|
||||
|
||||
[JsonProperty("nested_score_per_object")]
|
||||
public double NestedScorePerObject { get; set; }
|
||||
|
||||
@@ -84,11 +93,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("maximum_legacy_combo_score")]
|
||||
public double MaximumLegacyComboScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The beatmap's drain rate. This doesn't scale with rate-adjusting mods.
|
||||
/// </summary>
|
||||
public double DrainRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of hitcircles in the beatmap.
|
||||
/// </summary>
|
||||
@@ -111,6 +115,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
yield return (ATTRIB_ID_AIM, AimDifficulty);
|
||||
yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
|
||||
yield return (ATTRIB_ID_READING, ReadingDifficulty);
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
|
||||
if (ShouldSerializeFlashlightDifficulty())
|
||||
@@ -127,6 +132,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
yield return (ATTRIB_ID_NESTED_SCORE_PER_OBJECT, NestedScorePerObject);
|
||||
yield return (ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER, LegacyScoreBaseMultiplier);
|
||||
yield return (ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE, MaximumLegacyComboScore);
|
||||
yield return (ATTRIB_ID_READING_DIFFICULT_NOTE_COUNT, ReadingDifficultNoteCount);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
@@ -135,6 +141,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
AimDifficulty = values[ATTRIB_ID_AIM];
|
||||
SpeedDifficulty = values[ATTRIB_ID_SPEED];
|
||||
ReadingDifficulty = values[ATTRIB_ID_READING];
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
||||
@@ -147,7 +154,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
NestedScorePerObject = values[ATTRIB_ID_NESTED_SCORE_PER_OBJECT];
|
||||
LegacyScoreBaseMultiplier = values[ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER];
|
||||
MaximumLegacyComboScore = values[ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE];
|
||||
DrainRate = onlineInfo.DrainRate;
|
||||
ReadingDifficultNoteCount = values[ATTRIB_ID_READING_DIFFICULT_NOTE_COUNT];
|
||||
HitCircleCount = onlineInfo.CircleCount;
|
||||
SliderCount = onlineInfo.SliderCount;
|
||||
SpinnerCount = onlineInfo.SpinnerCount;
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||
@@ -16,14 +17,13 @@ using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
public class OsuDifficultyCalculator : DifficultyCalculator
|
||||
{
|
||||
private const double star_rating_multiplier = 0.0265;
|
||||
|
||||
public override int Version => 20250306;
|
||||
public override int Version => 20251020;
|
||||
|
||||
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return (79.5 - hitWindowGreat) / 6;
|
||||
}
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
||||
{
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
return new OsuDifficultyAttributes { Mods = mods };
|
||||
@@ -55,24 +55,30 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
var aimWithoutSliders = skills.OfType<Aim>().Single(a => !a.IncludeSliders);
|
||||
var speed = skills.OfType<Speed>().Single();
|
||||
var flashlight = skills.OfType<Flashlight>().SingleOrDefault();
|
||||
var reading = skills.OfType<Reading>().Single();
|
||||
|
||||
double aimDifficultyValue = aim.DifficultyValue();
|
||||
double aimNoSlidersDifficultyValue = aimWithoutSliders.DifficultyValue();
|
||||
double speedDifficultyValue = speed.DifficultyValue();
|
||||
double readingDifficultyValue = reading.DifficultyValue();
|
||||
|
||||
double aimDifficultStrainCount = aim.CountTopWeightedStrains(aimDifficultyValue);
|
||||
double speedDifficultStrainCount = speed.CountTopWeightedObjectDifficulties(speedDifficultyValue);
|
||||
double readingDifficultNoteCount = reading.CountTopWeightedObjectDifficulties(readingDifficultyValue);
|
||||
|
||||
double speedNotes = speed.RelevantNoteCount();
|
||||
|
||||
double aimDifficultStrainCount = aim.CountTopWeightedStrains();
|
||||
double speedDifficultStrainCount = speed.CountTopWeightedStrains();
|
||||
|
||||
double aimNoSlidersTopWeightedSliderCount = aimWithoutSliders.CountTopWeightedSliders();
|
||||
double aimNoSlidersDifficultStrainCount = aimWithoutSliders.CountTopWeightedStrains();
|
||||
double aimNoSlidersTopWeightedSliderCount = aimWithoutSliders.CountTopWeightedSliders(aimNoSlidersDifficultyValue);
|
||||
double aimNoSlidersDifficultStrainCount = aimWithoutSliders.CountTopWeightedStrains(aimNoSlidersDifficultyValue);
|
||||
|
||||
double aimTopWeightedSliderFactor = aimNoSlidersTopWeightedSliderCount / Math.Max(1, aimNoSlidersDifficultStrainCount - aimNoSlidersTopWeightedSliderCount);
|
||||
|
||||
double speedTopWeightedSliderCount = speed.CountTopWeightedSliders();
|
||||
double speedTopWeightedSliderCount = speed.CountTopWeightedSliders(speedDifficultyValue);
|
||||
double speedTopWeightedSliderFactor = speedTopWeightedSliderCount / Math.Max(1, speedDifficultStrainCount - speedTopWeightedSliderCount);
|
||||
|
||||
double difficultSliders = aim.GetDifficultSliders();
|
||||
|
||||
double approachRate = CalculateRateAdjustedApproachRate(beatmap.Difficulty.ApproachRate, clockRate);
|
||||
double overallDifficulty = CalculateRateAdjustedOverallDifficulty(beatmap.Difficulty.OverallDifficulty, clockRate);
|
||||
double overallDifficulty = CalculateRateAdjustedOverallDifficulty(beatmap.Difficulty.OverallDifficulty, ModUtils.CalculateRateWithMods(mods));
|
||||
|
||||
int hitCircleCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
||||
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
||||
@@ -80,19 +86,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
int totalHits = beatmap.HitObjects.Count;
|
||||
|
||||
double drainRate = beatmap.Difficulty.DrainRate;
|
||||
double sliderFactor = aimDifficultyValue > 0
|
||||
? OsuRatingCalculator.CalculateDifficultyRating(aimNoSlidersDifficultyValue) / OsuRatingCalculator.CalculateDifficultyRating(aimDifficultyValue)
|
||||
: 1;
|
||||
|
||||
double aimDifficultyValue = aim.DifficultyValue();
|
||||
double aimNoSlidersDifficultyValue = aimWithoutSliders.DifficultyValue();
|
||||
double speedDifficultyValue = speed.DifficultyValue();
|
||||
|
||||
double mechanicalDifficultyRating = calculateMechanicalDifficultyRating(aimDifficultyValue, speedDifficultyValue);
|
||||
double sliderFactor = aimDifficultyValue > 0 ? OsuRatingCalculator.CalculateDifficultyRating(aimNoSlidersDifficultyValue) / OsuRatingCalculator.CalculateDifficultyRating(aimDifficultyValue) : 1;
|
||||
|
||||
var osuRatingCalculator = new OsuRatingCalculator(mods, totalHits, approachRate, overallDifficulty, mechanicalDifficultyRating, sliderFactor);
|
||||
var osuRatingCalculator = new OsuRatingCalculator(totalHits, overallDifficulty);
|
||||
|
||||
double aimRating = osuRatingCalculator.ComputeAimRating(aimDifficultyValue);
|
||||
double speedRating = osuRatingCalculator.ComputeSpeedRating(speedDifficultyValue);
|
||||
double readingRating = osuRatingCalculator.ComputeReadingRating(readingDifficultyValue);
|
||||
|
||||
double flashlightRating = 0.0;
|
||||
|
||||
@@ -100,21 +102,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
flashlightRating = osuRatingCalculator.ComputeFlashlightRating(flashlight.DifficultyValue());
|
||||
|
||||
double sliderNestedScorePerObject = LegacyScoreUtils.CalculateNestedScorePerObject(beatmap, totalHits);
|
||||
double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(beatmap);
|
||||
double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(WorkingBeatmap.Beatmap);
|
||||
|
||||
var simulator = new OsuLegacyScoreSimulator();
|
||||
var scoreAttributes = simulator.Simulate(WorkingBeatmap, beatmap);
|
||||
|
||||
double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
|
||||
double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);
|
||||
double baseAimPerformance = OsuPerformanceCalculator.DifficultyToPerformance(aimRating);
|
||||
double baseSpeedPerformance = HarmonicSkill.DifficultyToPerformance(speedRating);
|
||||
double baseReadingPerformance = HarmonicSkill.DifficultyToPerformance(readingRating);
|
||||
double baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating);
|
||||
double baseCognitionPerformance = SumCognitionDifficulty(baseReadingPerformance, baseFlashlightPerformance);
|
||||
|
||||
double basePerformance =
|
||||
Math.Pow(
|
||||
Math.Pow(baseAimPerformance, 1.1) +
|
||||
Math.Pow(baseSpeedPerformance, 1.1) +
|
||||
Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
|
||||
);
|
||||
double basePerformance = DifficultyCalculationUtils.Norm(OsuPerformanceCalculator.PERFORMANCE_NORM_EXPONENT, baseAimPerformance, baseSpeedPerformance, baseCognitionPerformance);
|
||||
|
||||
double starRating = calculateStarRating(basePerformance);
|
||||
|
||||
@@ -127,12 +126,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
SpeedDifficulty = speedRating,
|
||||
SpeedNoteCount = speedNotes,
|
||||
FlashlightDifficulty = flashlightRating,
|
||||
ReadingDifficulty = readingRating,
|
||||
SliderFactor = sliderFactor,
|
||||
AimDifficultStrainCount = aimDifficultStrainCount,
|
||||
SpeedDifficultStrainCount = speedDifficultStrainCount,
|
||||
ReadingDifficultNoteCount = readingDifficultNoteCount,
|
||||
AimTopWeightedSliderFactor = aimTopWeightedSliderFactor,
|
||||
SpeedTopWeightedSliderFactor = speedTopWeightedSliderFactor,
|
||||
DrainRate = drainRate,
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
HitCircleCount = hitCircleCount,
|
||||
SliderCount = sliderCount,
|
||||
@@ -145,28 +145,29 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private double calculateMechanicalDifficultyRating(double aimDifficultyValue, double speedDifficultyValue)
|
||||
public static double SumCognitionDifficulty(double reading, double flashlight)
|
||||
{
|
||||
double aimValue = OsuStrainSkill.DifficultyToPerformance(OsuRatingCalculator.CalculateDifficultyRating(aimDifficultyValue));
|
||||
double speedValue = OsuStrainSkill.DifficultyToPerformance(OsuRatingCalculator.CalculateDifficultyRating(speedDifficultyValue));
|
||||
if (reading <= 0)
|
||||
return flashlight;
|
||||
|
||||
double totalValue = Math.Pow(Math.Pow(aimValue, 1.1) + Math.Pow(speedValue, 1.1), 1 / 1.1);
|
||||
if (flashlight <= 0)
|
||||
return reading;
|
||||
|
||||
return calculateStarRating(totalValue);
|
||||
// Nerf flashlight value in cognition sum when reading is greater than flashlight
|
||||
return DifficultyCalculationUtils.Norm(OsuPerformanceCalculator.PERFORMANCE_NORM_EXPONENT, reading, flashlight * Math.Clamp(flashlight / reading, 0.25, 1.0));
|
||||
}
|
||||
|
||||
private double calculateStarRating(double basePerformance)
|
||||
{
|
||||
if (basePerformance <= 0.00001)
|
||||
return 0;
|
||||
|
||||
return Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * star_rating_multiplier * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4);
|
||||
return Math.Cbrt(basePerformance * OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER);
|
||||
}
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods)
|
||||
{
|
||||
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
||||
|
||||
double clockRate = ModUtils.CalculateRateWithMods(mods);
|
||||
|
||||
// The first jump is formed by the first two hitobjects of the map.
|
||||
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
|
||||
for (int i = 1; i < beatmap.HitObjects.Count; i++)
|
||||
@@ -177,13 +178,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return objects;
|
||||
}
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
|
||||
{
|
||||
var skills = new List<Skill>
|
||||
{
|
||||
new Aim(mods, true),
|
||||
new Aim(mods, false),
|
||||
new Speed(mods)
|
||||
new Speed(mods),
|
||||
new Reading(mods)
|
||||
};
|
||||
|
||||
if (mods.Any(h => h is OsuModFlashlight))
|
||||
|
||||
@@ -115,9 +115,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double missCount = 0;
|
||||
|
||||
// If sliders in the map are hard - it's likely for player to drop sliderends
|
||||
// If map has easy sliders - it's more likely for player to sliderbreak
|
||||
double likelyMissedSliderendPortion = 0.04 + 0.06 * Math.Pow(Math.Min(attributes.AimTopWeightedSliderFactor, 1), 2);
|
||||
|
||||
// Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
|
||||
// In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
|
||||
double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
|
||||
// In classic scores we can't know the amount of dropped sliders so we estimate it
|
||||
double fullComboThreshold = attributes.MaxCombo - Math.Min(4 + likelyMissedSliderendPortion * attributes.SliderCount, attributes.SliderCount);
|
||||
|
||||
if (score.MaxCombo < fullComboThreshold)
|
||||
missCount = Math.Pow(fullComboThreshold / Math.Max(1.0, score.MaxCombo), 2.5);
|
||||
|
||||
@@ -21,6 +21,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("flashlight")]
|
||||
public double Flashlight { get; set; }
|
||||
|
||||
[JsonProperty("reading")]
|
||||
public double Reading { get; set; }
|
||||
|
||||
[JsonProperty("effective_miss_count")]
|
||||
public double EffectiveMissCount { get; set; }
|
||||
|
||||
@@ -48,6 +51,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
yield return new PerformanceDisplayAttribute(nameof(Speed), "Speed", Speed);
|
||||
yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy);
|
||||
yield return new PerformanceDisplayAttribute(nameof(Flashlight), "Flashlight Bonus", Flashlight);
|
||||
yield return new PerformanceDisplayAttribute(nameof(Reading), "Reading", Reading);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@@ -19,7 +20,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
public class OsuPerformanceCalculator : PerformanceCalculator
|
||||
{
|
||||
public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||
public const double PERFORMANCE_BASE_MULTIPLIER = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||
public const double PERFORMANCE_NORM_EXPONENT = 1.1;
|
||||
|
||||
private bool usingClassicSliderAccuracy;
|
||||
private bool usingScoreV2;
|
||||
@@ -50,14 +52,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
private double greatHitWindow;
|
||||
private double okHitWindow;
|
||||
private double mehHitWindow;
|
||||
|
||||
private double overallDifficulty;
|
||||
private double approachRate;
|
||||
private double drainRate;
|
||||
|
||||
private double? speedDeviation;
|
||||
|
||||
private double aimEstimatedSliderBreaks;
|
||||
private double speedEstimatedSliderBreaks;
|
||||
|
||||
public static double DifficultyToPerformance(double difficulty) => 4.0 * Math.Pow(difficulty, 3.0);
|
||||
|
||||
public OsuPerformanceCalculator()
|
||||
: base(new OsuRuleset())
|
||||
{
|
||||
@@ -95,11 +101,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
approachRate = OsuDifficultyCalculator.CalculateRateAdjustedApproachRate(difficulty.ApproachRate, clockRate);
|
||||
overallDifficulty = OsuDifficultyCalculator.CalculateRateAdjustedOverallDifficulty(difficulty.OverallDifficulty, clockRate);
|
||||
drainRate = difficulty.DrainRate;
|
||||
|
||||
double comboBasedEstimatedMissCount = calculateComboBasedEstimatedMissCount(osuAttributes);
|
||||
double? scoreBasedEstimatedMissCount = null;
|
||||
|
||||
if (usingClassicSliderAccuracy && score.LegacyTotalScore != null)
|
||||
if (usingClassicSliderAccuracy && !usingScoreV2 && score.LegacyTotalScore != null)
|
||||
{
|
||||
var legacyScoreMissCalculator = new OsuLegacyScoreMissCalculator(score, osuAttributes);
|
||||
scoreBasedEstimatedMissCount = legacyScoreMissCalculator.Calculate();
|
||||
@@ -115,6 +122,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
effectiveMissCount = Math.Max(countMiss, effectiveMissCount);
|
||||
effectiveMissCount = Math.Min(totalHits, effectiveMissCount);
|
||||
|
||||
if (effectiveMissCount > 0)
|
||||
{
|
||||
aimEstimatedSliderBreaks = calculateEstimatedSliderBreaks(osuAttributes.AimTopWeightedSliderFactor, osuAttributes);
|
||||
speedEstimatedSliderBreaks = calculateEstimatedSliderBreaks(osuAttributes.SpeedTopWeightedSliderFactor, osuAttributes);
|
||||
}
|
||||
|
||||
double multiplier = PERFORMANCE_BASE_MULTIPLIER;
|
||||
|
||||
if (score.Mods.Any(m => m is OsuModNoFail))
|
||||
@@ -140,15 +153,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
double aimValue = computeAimValue(score, osuAttributes);
|
||||
double speedValue = computeSpeedValue(score, osuAttributes);
|
||||
double accuracyValue = computeAccuracyValue(score, osuAttributes);
|
||||
double flashlightValue = computeFlashlightValue(score, osuAttributes);
|
||||
|
||||
double totalValue =
|
||||
Math.Pow(
|
||||
Math.Pow(aimValue, 1.1) +
|
||||
Math.Pow(speedValue, 1.1) +
|
||||
Math.Pow(accuracyValue, 1.1) +
|
||||
Math.Pow(flashlightValue, 1.1), 1.0 / 1.1
|
||||
) * multiplier;
|
||||
double readingValue = computeReadingValue(osuAttributes);
|
||||
double flashlightValue = computeFlashlightValue(score, osuAttributes);
|
||||
double cognitionValue = OsuDifficultyCalculator.SumCognitionDifficulty(readingValue, flashlightValue);
|
||||
|
||||
double totalValue = DifficultyCalculationUtils.Norm(PERFORMANCE_NORM_EXPONENT, aimValue, speedValue, accuracyValue, cognitionValue) * multiplier;
|
||||
|
||||
return new OsuPerformanceAttributes
|
||||
{
|
||||
@@ -156,6 +166,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
Speed = speedValue,
|
||||
Accuracy = accuracyValue,
|
||||
Flashlight = flashlightValue,
|
||||
Reading = readingValue,
|
||||
EffectiveMissCount = effectiveMissCount,
|
||||
ComboBasedEstimatedMissCount = comboBasedEstimatedMissCount,
|
||||
ScoreBasedEstimatedMissCount = scoreBasedEstimatedMissCount,
|
||||
@@ -194,16 +205,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
aimDifficulty *= sliderNerfFactor;
|
||||
}
|
||||
|
||||
double aimValue = OsuStrainSkill.DifficultyToPerformance(aimDifficulty);
|
||||
double aimValue = DifficultyToPerformance(aimDifficulty);
|
||||
|
||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||
double lengthBonus = 0.95 + 0.35 * Math.Min(1.0, totalHits / 2000.0) +
|
||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||
aimValue *= lengthBonus;
|
||||
|
||||
if (effectiveMissCount > 0)
|
||||
{
|
||||
aimEstimatedSliderBreaks = calculateEstimatedSliderBreaks(attributes.AimTopWeightedSliderFactor, attributes);
|
||||
|
||||
double relevantMissCount = Math.Min(effectiveMissCount + aimEstimatedSliderBreaks, totalImperfectHits + countSliderTickMiss);
|
||||
|
||||
aimValue *= calculateMissPenalty(relevantMissCount, attributes.AimDifficultStrainCount);
|
||||
@@ -211,10 +220,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
// TC bonuses are excluded when blinds is present as the increased visual difficulty is unimportant when notes cannot be seen.
|
||||
if (score.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);
|
||||
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * drainRate * drainRate);
|
||||
else if (score.Mods.Any(m => m is OsuModTraceable))
|
||||
{
|
||||
aimValue *= 1.0 + OsuRatingCalculator.CalculateVisibilityBonus(score.Mods, approachRate, sliderFactor: attributes.SliderFactor);
|
||||
aimValue *= 1.0 + calculateTraceableBonus(attributes.SliderFactor);
|
||||
}
|
||||
|
||||
aimValue *= accuracy;
|
||||
@@ -227,44 +236,33 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
if (score.Mods.Any(h => h is OsuModRelax) || speedDeviation == null)
|
||||
return 0.0;
|
||||
|
||||
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||
|
||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||
speedValue *= lengthBonus;
|
||||
double speedValue = HarmonicSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||
|
||||
if (effectiveMissCount > 0)
|
||||
{
|
||||
speedEstimatedSliderBreaks = calculateEstimatedSliderBreaks(attributes.SpeedTopWeightedSliderFactor, attributes);
|
||||
|
||||
double relevantMissCount = Math.Min(effectiveMissCount + speedEstimatedSliderBreaks, totalImperfectHits + countSliderTickMiss);
|
||||
|
||||
speedValue *= calculateMissPenalty(relevantMissCount, attributes.SpeedDifficultStrainCount);
|
||||
}
|
||||
|
||||
// TC bonuses are excluded when blinds is present as the increased visual difficulty is unimportant when notes cannot be seen.
|
||||
if (score.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 (score.Mods.Any(m => m is OsuModTraceable))
|
||||
{
|
||||
speedValue *= 1.0 + OsuRatingCalculator.CalculateVisibilityBonus(score.Mods, approachRate);
|
||||
}
|
||||
|
||||
double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes);
|
||||
speedValue *= speedHighDeviationMultiplier;
|
||||
|
||||
// Calculate accuracy assuming the worst case scenario
|
||||
double relevantTotalDiff = Math.Max(0, totalHits - attributes.SpeedNoteCount);
|
||||
double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
|
||||
double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat));
|
||||
double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk));
|
||||
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
|
||||
// An effective hit window is created based on the speed SR. The higher the speed difficulty, the shorter the hit window.
|
||||
// For example, a speed SR of 4.0 leads to an effective hit window of 20ms, which is OD 10.
|
||||
double effectiveHitWindow = 20 * Math.Pow(4 / attributes.SpeedDifficulty, 0.35);
|
||||
|
||||
// Scale the speed value with accuracy and OD.
|
||||
speedValue *= Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - overallDifficulty) / 2);
|
||||
// Find the proportion of 300s on speed notes assuming the hit window was the effective hit window.
|
||||
double effectiveAccuracy = DifficultyCalculationUtils.Erf(effectiveHitWindow / (double)speedDeviation);
|
||||
|
||||
// Scale speed value by normalized accuracy.
|
||||
speedValue *= Math.Pow(effectiveAccuracy, 2);
|
||||
|
||||
return speedValue;
|
||||
}
|
||||
@@ -294,20 +292,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
double accuracyValue = Math.Pow(1.52163, overallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
|
||||
|
||||
// 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));
|
||||
accuracyValue *= amountHitObjectsWithAccuracy < 1000
|
||||
? Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)
|
||||
: Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.1);
|
||||
|
||||
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
|
||||
if (score.Mods.Any(m => m is OsuModBlinds))
|
||||
accuracyValue *= 1.14;
|
||||
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
|
||||
else if (score.Mods.Any(m => m is OsuModTraceable))
|
||||
{
|
||||
// Decrease bonus for AR > 10
|
||||
accuracyValue *= 1 + 0.08 * DifficultyCalculationUtils.ReverseLerp(approachRate, 11.5, 10);
|
||||
}
|
||||
|
||||
if (score.Mods.Any(m => m is OsuModFlashlight))
|
||||
accuracyValue *= 1.02;
|
||||
|
||||
return accuracyValue;
|
||||
}
|
||||
|
||||
@@ -330,6 +327,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return flashlightValue;
|
||||
}
|
||||
|
||||
private double computeReadingValue(OsuDifficultyAttributes attributes)
|
||||
{
|
||||
double readingValue = HarmonicSkill.DifficultyToPerformance(attributes.ReadingDifficulty);
|
||||
|
||||
if (effectiveMissCount > 0)
|
||||
readingValue *= calculateMissPenalty(effectiveMissCount + aimEstimatedSliderBreaks, attributes.ReadingDifficultNoteCount);
|
||||
|
||||
// Scale the reading value with accuracy _harshly_.
|
||||
readingValue *= Math.Pow(accuracy, 3);
|
||||
|
||||
return readingValue;
|
||||
}
|
||||
|
||||
private double calculateComboBasedEstimatedMissCount(OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (attributes.SliderCount <= 0)
|
||||
@@ -339,9 +349,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
if (usingClassicSliderAccuracy)
|
||||
{
|
||||
// If sliders in the map are hard - it's likely for player to drop sliderends
|
||||
// If map has easy sliders - it's more likely for player to sliderbreak
|
||||
double likelyMissedSliderendPortion = 0.04 + 0.06 * Math.Pow(Math.Min(attributes.AimTopWeightedSliderFactor, 1), 2);
|
||||
|
||||
// Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
|
||||
// In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
|
||||
double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
|
||||
// In classic scores we can't know the amount of dropped sliders so we estimate it
|
||||
double fullComboThreshold = attributes.MaxCombo - Math.Min(4 + likelyMissedSliderendPortion * attributes.SliderCount, attributes.SliderCount);
|
||||
|
||||
if (scoreMaxCombo < fullComboThreshold)
|
||||
missCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
||||
@@ -376,19 +390,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
private double calculateEstimatedSliderBreaks(double topWeightedSliderFactor, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (!usingClassicSliderAccuracy || countOk == 0)
|
||||
int nonMissMistakes = countOk + countMeh;
|
||||
|
||||
if (!usingClassicSliderAccuracy || nonMissMistakes == 0)
|
||||
return 0;
|
||||
|
||||
double missedComboPercent = 1.0 - (double)scoreMaxCombo / attributes.MaxCombo;
|
||||
double estimatedSliderBreaks = Math.Min(countOk, effectiveMissCount * topWeightedSliderFactor);
|
||||
double estimatedSliderBreaks = Math.Min(nonMissMistakes, effectiveMissCount * topWeightedSliderFactor);
|
||||
|
||||
// Scores with more Oks are more likely to have slider breaks.
|
||||
double okAdjustment = ((countOk - estimatedSliderBreaks) + 0.5) / countOk;
|
||||
// Scores with more Oks and Mehs are more likely to have slider breaks.
|
||||
// We add an arbitrary value to both sides of the division to make it more stable on extreme ends.
|
||||
double nonMissMistakeAdjustment = (nonMissMistakes - estimatedSliderBreaks + 4.5) / (nonMissMistakes + 4);
|
||||
|
||||
// There is a low probability of extra slider breaks on effective miss counts close to 1, as score based calculations are good at indicating if only a single break occurred.
|
||||
estimatedSliderBreaks *= DifficultyCalculationUtils.Smoothstep(effectiveMissCount, 1, 2);
|
||||
|
||||
return estimatedSliderBreaks * okAdjustment * DifficultyCalculationUtils.Logistic(missedComboPercent, 0.33, 15);
|
||||
return estimatedSliderBreaks * nonMissMistakeAdjustment * DifficultyCalculationUtils.Logistic(missedComboPercent, 0.33, 15);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -470,7 +487,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
if (speedDeviation == null)
|
||||
return 0;
|
||||
|
||||
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||
double speedValue = HarmonicSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||
|
||||
// Decides a point where the PP value achieved compared to the speed deviation is assumed to be tapped improperly. Any PP above this point is considered "excess" speed difficulty.
|
||||
// This is used to cause PP above the cutoff to scale logarithmically towards the original speed value thus nerfing the value.
|
||||
@@ -489,10 +506,34 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return adjustedSpeedValue / speedValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a visibility bonus that is applicable to Traceable.
|
||||
/// </summary>
|
||||
private double calculateTraceableBonus(double sliderFactor = 1)
|
||||
{
|
||||
// We want to reward slider aim less, more so at lower AR
|
||||
double highApproachRateSliderVisibilityFactor = 0.5 + (Math.Pow(sliderFactor, 6) / 2);
|
||||
double lowApproachRateSliderVisibilityFactor = Math.Pow(sliderFactor, 6);
|
||||
|
||||
// Start from normal curve, rewarding lower AR up to AR7
|
||||
double traceableBonus = 0.0275;
|
||||
traceableBonus += 0.025 * (12.0 - Math.Max(approachRate, 7)) * highApproachRateSliderVisibilityFactor;
|
||||
|
||||
// For AR up to 0 - reduce reward for very low ARs when object is visible
|
||||
if (approachRate < 7)
|
||||
traceableBonus += 0.025 * (7.0 - Math.Max(approachRate, 0)) * lowApproachRateSliderVisibilityFactor;
|
||||
|
||||
// Starting from AR0 - cap values so they won't grow to infinity
|
||||
if (approachRate < 0)
|
||||
traceableBonus += 0.025 * (1 - Math.Pow(1.5, approachRate)) * lowApproachRateSliderVisibilityFactor;
|
||||
|
||||
return traceableBonus;
|
||||
}
|
||||
|
||||
// Miss penalty assumes that a player will miss on the hardest parts of a map,
|
||||
// so we use the amount of relatively difficult sections to adjust miss penalty
|
||||
// to make it more punishing on maps with lower amount of hard sections.
|
||||
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1);
|
||||
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.93 / (missCount / (4 * Math.Log(difficultStrainCount)) + 1);
|
||||
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
|
||||
|
||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
@@ -13,64 +9,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
private const double difficulty_multiplier = 0.0675;
|
||||
|
||||
private readonly Mod[] mods;
|
||||
private readonly int totalHits;
|
||||
private readonly double approachRate;
|
||||
private readonly double overallDifficulty;
|
||||
private readonly double mechanicalDifficultyRating;
|
||||
private readonly double sliderFactor;
|
||||
|
||||
public OsuRatingCalculator(Mod[] mods, int totalHits, double approachRate, double overallDifficulty, double mechanicalDifficultyRating, double sliderFactor)
|
||||
public OsuRatingCalculator(int totalHits, double overallDifficulty)
|
||||
{
|
||||
this.mods = mods;
|
||||
this.totalHits = totalHits;
|
||||
this.approachRate = approachRate;
|
||||
this.overallDifficulty = overallDifficulty;
|
||||
this.mechanicalDifficultyRating = mechanicalDifficultyRating;
|
||||
this.sliderFactor = sliderFactor;
|
||||
}
|
||||
|
||||
public double ComputeAimRating(double aimDifficultyValue)
|
||||
{
|
||||
if (mods.Any(m => m is OsuModAutopilot))
|
||||
return 0;
|
||||
|
||||
double aimRating = CalculateDifficultyRating(aimDifficultyValue);
|
||||
|
||||
if (mods.Any(m => m is OsuModTouchDevice))
|
||||
aimRating = Math.Pow(aimRating, 0.8);
|
||||
|
||||
if (mods.Any(m => m is OsuModRelax))
|
||||
aimRating *= 0.9;
|
||||
|
||||
if (mods.Any(m => m is OsuModMagnetised))
|
||||
{
|
||||
float magnetisedStrength = mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
|
||||
aimRating *= 1.0 - magnetisedStrength;
|
||||
}
|
||||
double aimRating = Math.Pow(aimDifficultyValue, 0.63) * 0.02275;
|
||||
|
||||
double ratingMultiplier = 1.0;
|
||||
|
||||
double approachRateLengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||
|
||||
double approachRateFactor = 0.0;
|
||||
if (approachRate > 10.33)
|
||||
approachRateFactor = 0.3 * (approachRate - 10.33);
|
||||
else if (approachRate < 8.0)
|
||||
approachRateFactor = 0.05 * (8.0 - approachRate);
|
||||
|
||||
if (mods.Any(h => h is OsuModRelax))
|
||||
approachRateFactor = 0.0;
|
||||
|
||||
ratingMultiplier += approachRateFactor * approachRateLengthBonus; // Buff for longer maps with high AR.
|
||||
|
||||
if (mods.Any(m => m is OsuModHidden))
|
||||
{
|
||||
double visibilityFactor = calculateAimVisibilityFactor(approachRate);
|
||||
ratingMultiplier += CalculateVisibilityBonus(mods, approachRate, visibilityFactor, sliderFactor);
|
||||
}
|
||||
|
||||
// It is important to consider accuracy difficulty when scaling with accuracy.
|
||||
ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
|
||||
|
||||
@@ -79,73 +32,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
public double ComputeSpeedRating(double speedDifficultyValue)
|
||||
{
|
||||
if (mods.Any(m => m is OsuModRelax))
|
||||
return 0;
|
||||
return CalculateDifficultyRating(speedDifficultyValue);
|
||||
}
|
||||
|
||||
double speedRating = CalculateDifficultyRating(speedDifficultyValue);
|
||||
|
||||
if (mods.Any(m => m is OsuModAutopilot))
|
||||
speedRating *= 0.5;
|
||||
|
||||
if (mods.Any(m => m is OsuModMagnetised))
|
||||
{
|
||||
// reduce speed rating because of the speed distance scaling, with maximum reduction being 0.7x
|
||||
float magnetisedStrength = mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
|
||||
speedRating *= 1.0 - magnetisedStrength * 0.3;
|
||||
}
|
||||
public double ComputeReadingRating(double readingDifficultyValue)
|
||||
{
|
||||
double readingRating = CalculateDifficultyRating(readingDifficultyValue);
|
||||
|
||||
double ratingMultiplier = 1.0;
|
||||
|
||||
double approachRateLengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||
ratingMultiplier *= 0.75 + Math.Pow(Math.Max(0, overallDifficulty), 2.2) / 800;
|
||||
|
||||
double approachRateFactor = 0.0;
|
||||
if (approachRate > 10.33)
|
||||
approachRateFactor = 0.3 * (approachRate - 10.33);
|
||||
|
||||
if (mods.Any(m => m is OsuModAutopilot))
|
||||
approachRateFactor = 0.0;
|
||||
|
||||
ratingMultiplier += approachRateFactor * approachRateLengthBonus; // Buff for longer maps with high AR.
|
||||
|
||||
if (mods.Any(m => m is OsuModHidden))
|
||||
{
|
||||
double visibilityFactor = calculateSpeedVisibilityFactor(approachRate);
|
||||
ratingMultiplier += CalculateVisibilityBonus(mods, approachRate, visibilityFactor);
|
||||
}
|
||||
|
||||
ratingMultiplier *= 0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750;
|
||||
|
||||
return speedRating * Math.Cbrt(ratingMultiplier);
|
||||
return readingRating * Math.Cbrt(ratingMultiplier);
|
||||
}
|
||||
|
||||
public double ComputeFlashlightRating(double flashlightDifficultyValue)
|
||||
{
|
||||
if (!mods.Any(m => m is OsuModFlashlight))
|
||||
return 0;
|
||||
|
||||
double flashlightRating = CalculateDifficultyRating(flashlightDifficultyValue);
|
||||
|
||||
if (mods.Any(m => m is OsuModTouchDevice))
|
||||
flashlightRating = Math.Pow(flashlightRating, 0.8);
|
||||
|
||||
if (mods.Any(m => m is OsuModRelax))
|
||||
flashlightRating *= 0.7;
|
||||
else if (mods.Any(m => m is OsuModAutopilot))
|
||||
flashlightRating *= 0.4;
|
||||
|
||||
if (mods.Any(m => m is OsuModMagnetised))
|
||||
{
|
||||
float magnetisedStrength = mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
|
||||
flashlightRating *= 1.0 - magnetisedStrength;
|
||||
}
|
||||
|
||||
if (mods.Any(m => m is OsuModDeflate))
|
||||
{
|
||||
float deflateInitialScale = mods.OfType<OsuModDeflate>().First().StartScale.Value;
|
||||
flashlightRating *= Math.Clamp(DifficultyCalculationUtils.ReverseLerp(deflateInitialScale, 11, 1), 0.1, 1);
|
||||
}
|
||||
|
||||
double ratingMultiplier = 1.0;
|
||||
|
||||
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
||||
@@ -158,56 +62,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return flashlightRating * Math.Sqrt(ratingMultiplier);
|
||||
}
|
||||
|
||||
private double calculateAimVisibilityFactor(double approachRate)
|
||||
{
|
||||
const double ar_factor_end_point = 11.5;
|
||||
|
||||
double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10);
|
||||
double arFactorStartingPoint = double.Lerp(9, 10.33, mechanicalDifficultyFactor);
|
||||
|
||||
return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint);
|
||||
}
|
||||
|
||||
private double calculateSpeedVisibilityFactor(double approachRate)
|
||||
{
|
||||
const double ar_factor_end_point = 11.5;
|
||||
|
||||
double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10);
|
||||
double arFactorStartingPoint = double.Lerp(10, 10.33, mechanicalDifficultyFactor);
|
||||
|
||||
return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a visibility bonus that is applicable to Hidden and Traceable.
|
||||
/// </summary>
|
||||
public static double CalculateVisibilityBonus(Mod[] mods, double approachRate, double visibilityFactor = 1, double sliderFactor = 1)
|
||||
{
|
||||
// NOTE: TC's effect is only noticeable in performance calculations until lazer mods are accounted for server-side.
|
||||
bool isAlwaysPartiallyVisible = mods.OfType<OsuModHidden>().Any(m => m.OnlyFadeApproachCircles.Value) || mods.OfType<OsuModTraceable>().Any();
|
||||
|
||||
// Start from normal curve, rewarding lower AR up to AR7
|
||||
// TC forcefully requires a lower reading bonus for now as it's post-applied in PP which makes it multiplicative with the regular AR bonuses
|
||||
// This means it has an advantage over HD, so we decrease the multiplier to compensate
|
||||
// This should be removed once we're able to apply TC bonuses in SR (depends on real-time difficulty calculations being possible)
|
||||
double readingBonus = (isAlwaysPartiallyVisible ? 0.025 : 0.04) * (12.0 - Math.Max(approachRate, 7));
|
||||
|
||||
readingBonus *= visibilityFactor;
|
||||
|
||||
// We want to reward slideraim on low AR less
|
||||
double sliderVisibilityFactor = Math.Pow(sliderFactor, 3);
|
||||
|
||||
// For AR up to 0 - reduce reward for very low ARs when object is visible
|
||||
if (approachRate < 7)
|
||||
readingBonus += (isAlwaysPartiallyVisible ? 0.02 : 0.045) * (7.0 - Math.Max(approachRate, 0)) * sliderVisibilityFactor;
|
||||
|
||||
// Starting from AR0 - cap values so they won't grow to infinity
|
||||
if (approachRate < 0)
|
||||
readingBonus += (isAlwaysPartiallyVisible ? 0.01 : 0.1) * (1 - Math.Pow(1.5, approachRate)) * sliderVisibilityFactor;
|
||||
|
||||
return readingBonus;
|
||||
}
|
||||
|
||||
public static double CalculateDifficultyRating(double difficultyValue) => Math.Sqrt(difficultyValue) * difficulty_multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@@ -35,6 +36,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
/// </summary>
|
||||
public readonly double AdjustedDeltaTime;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of time elapsed between lastDifficultyObject's <see cref="DifficultyHitObject.EndTime"/> and <see cref="DifficultyHitObject.StartTime"/> capped to a minimum of <see cref="MIN_DELTA_TIME"/>ms.
|
||||
/// </summary>
|
||||
public double LastObjectEndDeltaTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time (in ms) between the object first appearing and the time it needs to be clicked.
|
||||
/// <see cref="OsuHitObject.TimePreempt"/> adjusted by clock rate.
|
||||
/// </summary>
|
||||
public readonly double Preempt;
|
||||
|
||||
/// <summary>
|
||||
/// Normalised distance from the start position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
public double JumpDistance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalised distance from the "lazy" end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
|
||||
/// <para>
|
||||
@@ -101,9 +118,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
public double? Angle { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the full hit window for a Great <see cref="HitResult"/>.
|
||||
/// Angle of the vector created between current and current-1
|
||||
/// normalised to consider symmetrical vectors in any axis to be the same angle.
|
||||
/// </summary>
|
||||
public double HitWindowGreat { get; private set; }
|
||||
public double? NormalisedVectorAngle { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Selective bonus for maps with higher circle size.
|
||||
@@ -121,17 +139,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
|
||||
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
||||
AdjustedDeltaTime = Math.Max(DeltaTime, MIN_DELTA_TIME);
|
||||
LastObjectEndDeltaTime = lastDifficultyObject != null ? Math.Max(StartTime - lastDifficultyObject.EndTime, MIN_DELTA_TIME) : AdjustedDeltaTime;
|
||||
|
||||
SmallCircleBonus = Math.Max(1.0, 1.0 + (30 - BaseObject.Radius) / 40);
|
||||
SmallCircleBonus = Math.Max(1.0, 1.0 + (30 - BaseObject.Radius) / 70);
|
||||
|
||||
if (BaseObject is Slider sliderObject)
|
||||
{
|
||||
HitWindowGreat = 2 * sliderObject.HeadCircle.HitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
}
|
||||
else
|
||||
{
|
||||
HitWindowGreat = 2 * BaseObject.HitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
}
|
||||
Preempt = BaseObject.TimePreempt / clockRate;
|
||||
|
||||
computeSliderCursorPosition();
|
||||
setDistances(clockRate);
|
||||
@@ -148,7 +160,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
}
|
||||
|
||||
double fadeInStartTime = BaseObject.StartTime - BaseObject.TimePreempt;
|
||||
double fadeInDuration = BaseObject.TimeFadeIn;
|
||||
|
||||
// Equal to `OsuHitObject.TimeFadeIn` minus any adjustments from the HD mod.
|
||||
double fadeInDuration = 400 * Math.Min(1, BaseObject.TimePreempt / OsuHitObject.PREEMPT_MIN);
|
||||
|
||||
if (hidden)
|
||||
{
|
||||
@@ -175,10 +189,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
{
|
||||
double currDeltaTime = Math.Max(1, DeltaTime);
|
||||
double nextDeltaTime = Math.Max(1, osuNextObj.DeltaTime);
|
||||
|
||||
double deltaDifference = Math.Abs(nextDeltaTime - currDeltaTime);
|
||||
|
||||
double speedRatio = currDeltaTime / Math.Max(currDeltaTime, deltaDifference);
|
||||
double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / HitWindowGreat), 2);
|
||||
return 1.0 - Math.Pow(speedRatio, 1 - windowRatio);
|
||||
double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / HitWindow(HitResult.Great)), 5);
|
||||
|
||||
// Can't doubletap if circles don't intersect
|
||||
double distanceFactor = Math.Pow(DifficultyCalculationUtils.ReverseLerp(LazyJumpDistance, NORMALISED_DIAMETER, NORMALISED_RADIUS), 2);
|
||||
|
||||
return 1.0 - Math.Pow(speedRatio, distanceFactor * (1 - windowRatio));
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -189,10 +209,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
if (BaseObject is Slider currentSlider)
|
||||
{
|
||||
// Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
||||
TravelDistance = LazyTravelDistance * Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5);
|
||||
TravelDistance = LazyTravelDistance * Math.Max(1, Math.Pow(currentSlider.RepeatCount, 0.3));
|
||||
TravelTime = Math.Max(LazyTravelTime / clockRate, MIN_DELTA_TIME);
|
||||
}
|
||||
|
||||
MinimumJumpTime = AdjustedDeltaTime;
|
||||
|
||||
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
|
||||
if (BaseObject is Spinner || LastObject is Spinner)
|
||||
return;
|
||||
@@ -202,8 +224,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
|
||||
Vector2 lastCursorPosition = lastDifficultyObject != null ? getEndCursorPosition(lastDifficultyObject) : LastObject.StackedPosition;
|
||||
|
||||
LazyJumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
|
||||
MinimumJumpTime = AdjustedDeltaTime;
|
||||
JumpDistance = (LastObject.StackedPosition - BaseObject.StackedPosition).Length * scalingFactor;
|
||||
LazyJumpDistance = (BaseObject.StackedPosition - lastCursorPosition).Length * scalingFactor;
|
||||
MinimumJumpDistance = LazyJumpDistance;
|
||||
|
||||
if (LastObject is Slider lastSlider && lastDifficultyObject != null)
|
||||
@@ -239,15 +261,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
|
||||
if (lastLastDifficultyObject != null && lastLastDifficultyObject.BaseObject is not Spinner)
|
||||
{
|
||||
if (lastDifficultyObject!.BaseObject is Slider prevSlider && lastDifficultyObject.TravelDistance > 0)
|
||||
lastCursorPosition = prevSlider.HeadCircle.StackedPosition;
|
||||
|
||||
Vector2 lastLastCursorPosition = getEndCursorPosition(lastLastDifficultyObject);
|
||||
|
||||
Vector2 v1 = lastLastCursorPosition - LastObject.StackedPosition;
|
||||
Vector2 v2 = BaseObject.StackedPosition - lastCursorPosition;
|
||||
double angle = calculateAngle(BaseObject.StackedPosition, lastCursorPosition, lastLastCursorPosition);
|
||||
double sliderAngle = calculateSliderAngle(lastDifficultyObject!, lastLastCursorPosition);
|
||||
|
||||
float dot = Vector2.Dot(v1, v2);
|
||||
float det = v1.X * v2.Y - v1.Y * v2.X;
|
||||
Vector2 v = BaseObject.StackedPosition - lastCursorPosition;
|
||||
NormalisedVectorAngle = Math.Atan2(Math.Abs(v.Y), Math.Abs(v.X));
|
||||
|
||||
Angle = Math.Abs(Math.Atan2(det, dot));
|
||||
Angle = Math.Min(angle, sliderAngle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,6 +384,30 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
}
|
||||
}
|
||||
|
||||
private double calculateSliderAngle(OsuDifficultyHitObject lastDifficultyObject, Vector2 lastLastCursorPosition)
|
||||
{
|
||||
Vector2 lastCursorPosition = getEndCursorPosition(lastDifficultyObject);
|
||||
|
||||
if (lastDifficultyObject.BaseObject is Slider prevSlider && lastDifficultyObject.TravelDistance > 0)
|
||||
{
|
||||
OsuHitObject secondLastNestedObject = (OsuHitObject)prevSlider.NestedHitObjects[^2];
|
||||
lastLastCursorPosition = secondLastNestedObject.StackedPosition;
|
||||
}
|
||||
|
||||
return calculateAngle(BaseObject.StackedPosition, lastCursorPosition, lastLastCursorPosition);
|
||||
}
|
||||
|
||||
private double calculateAngle(Vector2 currentPosition, Vector2 lastPosition, Vector2 lastLastPosition)
|
||||
{
|
||||
Vector2 v1 = lastLastPosition - lastPosition;
|
||||
Vector2 v2 = currentPosition - lastPosition;
|
||||
|
||||
float dot = Vector2.Dot(v1, v2);
|
||||
float det = v1.X * v2.Y - v1.Y * v2.X;
|
||||
|
||||
return Math.Abs(Math.Atan2(det, dot));
|
||||
}
|
||||
|
||||
private Vector2 getEndCursorPosition(OsuDifficultyHitObject difficultyHitObject)
|
||||
{
|
||||
return difficultyHitObject.LazyEndPosition ?? difficultyHitObject.BaseObject.StackedPosition;
|
||||
|
||||
@@ -4,10 +4,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators.Aim;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
@@ -15,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// <summary>
|
||||
/// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
||||
/// </summary>
|
||||
public class Aim : OsuStrainSkill
|
||||
public class Aim : VariableLengthStrainSkill
|
||||
{
|
||||
public readonly bool IncludeSliders;
|
||||
|
||||
@@ -27,19 +31,39 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
private double skillMultiplier => 26;
|
||||
private double strainDecayBase => 0.15;
|
||||
private double skillMultiplierSnap => 70.9;
|
||||
private double skillMultiplierAgility => 2.35;
|
||||
private double skillMultiplierFlow => 242.0;
|
||||
private double skillMultiplierTotal => 1.12;
|
||||
private double combinedSnapNormExponent => 1.2;
|
||||
|
||||
/// <summary>
|
||||
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
|
||||
/// This is done in order to decrease their impact on the overall difficulty of the map for this skill.
|
||||
/// </summary>
|
||||
private int reducedSectionTime => 4000;
|
||||
|
||||
/// <summary>
|
||||
/// The baseline multiplier applied to the section with the biggest strain.
|
||||
/// </summary>
|
||||
private double reducedStrainBaseline => 0.727;
|
||||
|
||||
private readonly List<double> sliderStrains = new List<double>();
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
private double strainDecay(double ms) => Math.Pow(0.2, ms / 1000);
|
||||
|
||||
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime);
|
||||
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) =>
|
||||
currentStrain * strainDecay(time - current.Previous(0).StartTime);
|
||||
|
||||
protected override double StrainValueAt(DifficultyHitObject current)
|
||||
{
|
||||
currentStrain *= strainDecay(current.DeltaTime);
|
||||
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplier;
|
||||
if (Mods.Any(m => m is OsuModAutopilot))
|
||||
return 0;
|
||||
|
||||
double decay = strainDecay(((OsuDifficultyHitObject)current).AdjustedDeltaTime);
|
||||
|
||||
currentStrain *= decay;
|
||||
currentStrain += calculateModAdjustedDifficulty(current) * (1 - decay);
|
||||
|
||||
if (current.BaseObject is Slider)
|
||||
sliderStrains.Add(currentStrain);
|
||||
@@ -47,6 +71,73 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
return currentStrain;
|
||||
}
|
||||
|
||||
private double calculateModAdjustedDifficulty(DifficultyHitObject current)
|
||||
{
|
||||
double snapDifficulty = SnapAimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplierSnap;
|
||||
double agilityDifficulty = AgilityEvaluator.EvaluateDifficultyOf(current) * skillMultiplierAgility;
|
||||
double flowDifficulty = FlowAimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplierFlow;
|
||||
|
||||
double totalDifficulty = calculateTotalValue(snapDifficulty, agilityDifficulty, flowDifficulty);
|
||||
|
||||
if (Mods.Any(m => m is OsuModMagnetised))
|
||||
{
|
||||
float magnetisedStrength = Mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
|
||||
totalDifficulty *= 1.0 - magnetisedStrength;
|
||||
}
|
||||
|
||||
return totalDifficulty;
|
||||
}
|
||||
|
||||
private double calculateTotalValue(double snapDifficulty, double agilityDifficulty, double flowDifficulty)
|
||||
{
|
||||
// We compare flow to combined snap and agility because snap by itself doesn't have enough difficulty to be above flow on streams
|
||||
// Agility on the other hand is supposed to measure the rate of cursor velocity changes while snapping
|
||||
// So snapping every circle on a stream requires an enormous amount of agility at which point it's easier to flow
|
||||
double combinedSnapDifficulty = DifficultyCalculationUtils.Norm(combinedSnapNormExponent, snapDifficulty, agilityDifficulty);
|
||||
|
||||
double pSnap = calculateSnapFlowProbability(flowDifficulty / combinedSnapDifficulty);
|
||||
double pFlow = 1 - pSnap;
|
||||
|
||||
if (Mods.Any(m => m is OsuModTouchDevice))
|
||||
{
|
||||
// we don't adjust agility here since agility represents TD difficulty in a decent enough way
|
||||
snapDifficulty = Math.Pow(snapDifficulty, 0.89);
|
||||
combinedSnapDifficulty = DifficultyCalculationUtils.Norm(combinedSnapNormExponent, snapDifficulty, agilityDifficulty);
|
||||
}
|
||||
|
||||
if (Mods.Any(m => m is OsuModRelax))
|
||||
{
|
||||
combinedSnapDifficulty *= 0.75;
|
||||
flowDifficulty *= 0.6;
|
||||
}
|
||||
|
||||
double totalDifficulty = combinedSnapDifficulty * pSnap + flowDifficulty * pFlow;
|
||||
|
||||
double totalStrain = totalDifficulty * skillMultiplierTotal;
|
||||
|
||||
return totalStrain;
|
||||
}
|
||||
|
||||
// A function that turns the ratio of snap : flow into the probability of snapping/flowing
|
||||
// It has the constraints:
|
||||
// P(snap) + P(flow) = 1 (the object is always either snapped or flowed)
|
||||
// P(snap) = f(snap/flow), P(flow) = f(flow/snap) (ie snap and flow are symmetric and reversible)
|
||||
// Therefore: f(x) + f(1/x) = 1
|
||||
// 0 <= f(x) <= 1 (cannot have negative or greater than 100% probability of snapping or flowing)
|
||||
// This logistic function is a solution, which fits nicely with the general idea of interpolation and provides a tuneable constant
|
||||
private static double calculateSnapFlowProbability(double ratio)
|
||||
{
|
||||
const double k = 7.27;
|
||||
|
||||
if (ratio == 0)
|
||||
return 0;
|
||||
|
||||
if (double.IsNaN(ratio))
|
||||
return 1;
|
||||
|
||||
return DifficultyCalculationUtils.Logistic(-k * Math.Log(ratio));
|
||||
}
|
||||
|
||||
public double GetDifficultSliders()
|
||||
{
|
||||
if (sliderStrains.Count == 0)
|
||||
@@ -60,6 +151,97 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
return sliderStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0))));
|
||||
}
|
||||
|
||||
public double CountTopWeightedSliders() => OsuStrainUtils.CountTopWeightedSliders(sliderStrains, DifficultyValue());
|
||||
public double CountTopWeightedSliders(double difficultyValue)
|
||||
{
|
||||
if (sliderStrains.Count == 0)
|
||||
return 0;
|
||||
|
||||
double consistentTopStrain = difficultyValue * (1 - DecayWeight); // What would the top strain be if all strain values were identical
|
||||
|
||||
if (consistentTopStrain == 0)
|
||||
return 0;
|
||||
|
||||
// Use a weighted sum of all strains. Constants are arbitrary and give nice values
|
||||
return sliderStrains.Sum(s => DifficultyCalculationUtils.Logistic(s / consistentTopStrain, 0.88, 10, 1.1));
|
||||
}
|
||||
|
||||
public override double DifficultyValue()
|
||||
{
|
||||
double difficulty = 0;
|
||||
double time = 0;
|
||||
|
||||
var strains = getReducedStrainPeaks();
|
||||
|
||||
// Difficulty is a continuous weighted sum of the sorted strains
|
||||
foreach (StrainPeak strain in strains)
|
||||
{
|
||||
/* Weighting function can be thought of as:
|
||||
b
|
||||
∫ DecayWeight^x dx
|
||||
a
|
||||
where a = startTime and b = endTime
|
||||
|
||||
Technically, the function below has been slightly modified from the equation above.
|
||||
The real function would be
|
||||
double weight = Math.Pow(DecayWeight, startTime) - Math.Pow(DecayWeight, endTime);
|
||||
...
|
||||
return difficulty / Math.Log(1 / DecayWeight);
|
||||
E.g. for a DecayWeight of 0.9, we're multiplying by 10 instead of 9.49122...
|
||||
|
||||
This change makes it so that a map composed solely of MaxSectionLength chunks will have the exact same value when summed in this class and StrainSkill.
|
||||
Doing this ensures the relationship between strain values and difficulty values remains the same between the two classes.
|
||||
*/
|
||||
double startTime = time;
|
||||
double endTime = time + strain.SectionLength / MaxSectionLength;
|
||||
|
||||
double weight = Math.Pow(DecayWeight, startTime) - Math.Pow(DecayWeight, endTime);
|
||||
|
||||
difficulty += strain.Value * weight;
|
||||
time = endTime;
|
||||
}
|
||||
|
||||
return difficulty / (1 - DecayWeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a sorted enumerable of strain peaks with the highest values reduced.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private IEnumerable<StrainPeak> getReducedStrainPeaks()
|
||||
{
|
||||
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
||||
// These sections will not contribute to the difficulty.
|
||||
var peaks = GetCurrentStrainPeaks().Where(p => p.Value > 0);
|
||||
|
||||
List<StrainPeak> strains = peaks.OrderByDescending(p => p.Value).ToList();
|
||||
|
||||
const int chunk_size = 20;
|
||||
double time = 0;
|
||||
int strainsToRemove = 0; // All strains are removed at the end for optimization purposes
|
||||
|
||||
// We are reducing the highest strains first to account for extreme difficulty spikes
|
||||
// Strains are split into 20ms chunks to try to mitigate inconsistencies caused by reducing strains
|
||||
while (strains.Count > strainsToRemove && time < reducedSectionTime)
|
||||
{
|
||||
StrainPeak strain = strains[strainsToRemove];
|
||||
|
||||
for (double addedTime = 0; addedTime < strain.SectionLength; addedTime += chunk_size)
|
||||
{
|
||||
double scale = Math.Log10(Interpolation.Lerp(1, 10, Math.Clamp((time + addedTime) / reducedSectionTime, 0, 1)));
|
||||
|
||||
strains.Add(new StrainPeak(
|
||||
strain.Value * Interpolation.Lerp(reducedStrainBaseline, 1.0, scale),
|
||||
Math.Min(chunk_size, strain.SectionLength - addedTime)
|
||||
));
|
||||
}
|
||||
|
||||
time += strain.SectionLength;
|
||||
strainsToRemove++;
|
||||
}
|
||||
|
||||
strains.RemoveRange(0, strainsToRemove);
|
||||
|
||||
return strains.OrderByDescending(p => p.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user