Compare commits
1202 Commits
@@ -19,6 +19,11 @@ indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130051/Cannot-resolve-symbol-inspections-incorrectly-firing-for-xmldoc-protected-member-references
|
||||
resharper_c_sharp_warnings_cs1574_cs1584_cs1581_cs1580_highlighting = hint
|
||||
# temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130381/Rider-does-not-respect-propagated-NoWarn-CS1591?backToIssues=false
|
||||
dotnet_diagnostic.CS1591.severity = none
|
||||
|
||||
#license header
|
||||
file_header_template = Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text.
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
|
||||
build-only-ios:
|
||||
name: Build only (iOS)
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -143,7 +143,12 @@ jobs:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
- name: Install .NET Workloads
|
||||
run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json
|
||||
run: dotnet workload install ios
|
||||
|
||||
# https://github.com/dotnet/macios/issues/19157
|
||||
# https://github.com/actions/runner-images/issues/12758
|
||||
- name: Use Xcode 16.4
|
||||
run: sudo xcode-select -switch /Applications/Xcode_16.4.app
|
||||
|
||||
- name: Build
|
||||
run: dotnet build -c Debug osu.iOS.slnf
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.710.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.930.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Android.App;
|
||||
using Android.Content.PM;
|
||||
using Microsoft.Maui.Devices;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game;
|
||||
@@ -21,58 +23,30 @@ namespace osu.Android
|
||||
[Cached]
|
||||
private readonly OsuGameActivity gameActivity;
|
||||
|
||||
private readonly PackageInfo packageInfo;
|
||||
|
||||
public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth);
|
||||
|
||||
public OsuGameAndroid(OsuGameActivity activity)
|
||||
: base(null)
|
||||
{
|
||||
gameActivity = activity;
|
||||
packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull();
|
||||
}
|
||||
|
||||
public override Version AssemblyVersion
|
||||
public override string Version
|
||||
{
|
||||
get
|
||||
{
|
||||
var packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull();
|
||||
if (!IsDeployedBuild)
|
||||
return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release");
|
||||
|
||||
try
|
||||
{
|
||||
// We store the osu! build number in the "VersionCode" field to better support google play releases.
|
||||
// If we were to use the main build number, it would require a new submission each time (similar to TestFlight).
|
||||
// In order to do this, we should split it up and pad the numbers to still ensure sequential increase over time.
|
||||
//
|
||||
// We also need to be aware that older SDK versions store this as a 32bit int.
|
||||
//
|
||||
// Basic conversion format (as done in Fastfile): 2020.606.0 -> 202006060
|
||||
|
||||
// https://stackoverflow.com/questions/52977079/android-sdk-28-versioncode-in-packageinfo-has-been-deprecated
|
||||
string versionName;
|
||||
|
||||
if (OperatingSystem.IsAndroidVersionAtLeast(28))
|
||||
{
|
||||
versionName = packageInfo.LongVersionCode.ToString();
|
||||
// ensure we only read the trailing portion of long (the part we are interested in).
|
||||
versionName = versionName.Substring(versionName.Length - 9);
|
||||
}
|
||||
else
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
// this is required else older SDKs will report missing method exception.
|
||||
versionName = packageInfo.VersionCode.ToString();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
// undo play store version garbling (as mentioned above).
|
||||
return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1)));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return new Version(packageInfo.VersionName.AsNonNull());
|
||||
return packageInfo.VersionName.AsNonNull();
|
||||
}
|
||||
}
|
||||
|
||||
public override Version AssemblyVersion => new Version(packageInfo.VersionName.AsNonNull().Split('-').First());
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
@@ -123,7 +123,7 @@ namespace osu.Desktop
|
||||
|
||||
public override bool RestartAppWhenExited()
|
||||
{
|
||||
Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget();
|
||||
Task.Run(() => Velopack.UpdateExe.Start(waitPid: (uint)Environment.ProcessId)).FireAndForget();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace osu.Desktop
|
||||
// IMPORTANT DON'T IGNORE: For general sanity, velopack's setup needs to run before anything else.
|
||||
// This has bitten us in the rear before (bricked updater), and although the underlying issue from
|
||||
// last time has been fixed, let's not tempt fate.
|
||||
setupVelopack();
|
||||
setupVelopack(args);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
@@ -174,8 +174,21 @@ namespace osu.Desktop
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void setupVelopack()
|
||||
private static void setupVelopack(string[] args)
|
||||
{
|
||||
// Arguments being present indicate the user is either starting the game in a special (aka tournament) mode,
|
||||
// or is running with pending imports via file association or otherwise.
|
||||
//
|
||||
// In both these scenarios, we'd hope the game does not attempt to update.
|
||||
//
|
||||
// Special consideration for velopack startup arguments, which must be handled during update.
|
||||
// See https://docs.velopack.io/integrating/hooks#command-line-hooks.
|
||||
if (args.Length > 0 && !args[0].StartsWith("--velo", StringComparison.Ordinal))
|
||||
{
|
||||
Logger.Log("Handling arguments, skipping velopack setup.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (OsuGameDesktop.IsPackageManaged)
|
||||
{
|
||||
Logger.Log("Updates are being managed by an external provider. Skipping Velopack setup.");
|
||||
|
||||
@@ -53,33 +53,44 @@ namespace osu.Desktop.Updater
|
||||
return false;
|
||||
}
|
||||
|
||||
IUpdateSource updateSource = new GithubSource(@"https://github.com/ppy/osu", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon);
|
||||
Velopack.UpdateManager updateManager = new Velopack.UpdateManager(updateSource, new UpdateOptions
|
||||
try
|
||||
{
|
||||
AllowVersionDowngrade = true
|
||||
});
|
||||
IUpdateSource updateSource = new GithubSource(@"https://github.com/ppy/osu", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon);
|
||||
Velopack.UpdateManager updateManager = new Velopack.UpdateManager(updateSource, new UpdateOptions
|
||||
{
|
||||
AllowVersionDowngrade = true
|
||||
});
|
||||
|
||||
UpdateInfo? update = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
|
||||
UpdateInfo? update = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
log("Update check cancelled");
|
||||
scheduleNextUpdateCheck();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (update == null)
|
||||
{
|
||||
// No update is available.
|
||||
log("No update found");
|
||||
scheduleNextUpdateCheck();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Download update in the background while notifying awaiters of the update being available.
|
||||
log($"New update available: {update.TargetFullRelease.Version}");
|
||||
downloadUpdate(updateManager, update, cancellationToken);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log("Update check cancelled");
|
||||
log($"Update check failed with error ({e.Message})");
|
||||
|
||||
// we shouldn't crash on a web failure. or any failure for the matter.
|
||||
scheduleNextUpdateCheck();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (update == null)
|
||||
{
|
||||
// No update is available.
|
||||
log("No update found");
|
||||
scheduleNextUpdateCheck();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Download update in the background while notifying awaiters of the update being available.
|
||||
log($"New update available: {update.TargetFullRelease.Version}");
|
||||
downloadUpdate(updateManager, update, cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void downloadUpdate(Velopack.UpdateManager updateManager, UpdateInfo update, CancellationToken cancellationToken) => Task.Run(async () =>
|
||||
|
||||
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
@@ -0,0 +1,77 @@
|
||||
// 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.IO;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Benchmarks
|
||||
{
|
||||
public class BenchmarkDifficultyCalculation : BenchmarkTest
|
||||
{
|
||||
private DifficultyCalculator osuCalculator = null!;
|
||||
private DifficultyCalculator taikoCalculator = null!;
|
||||
private DifficultyCalculator catchCalculator = null!;
|
||||
private DifficultyCalculator maniaCalculator = null!;
|
||||
|
||||
public override void SetUp()
|
||||
{
|
||||
using var resources = new DllResourceStore(typeof(TestResources).Assembly);
|
||||
|
||||
using var archive = resources.GetStream("Resources/Archives/241526 Soleily - Renatus.osz");
|
||||
using var archiveReader = new ZipArchiveReader(archive);
|
||||
|
||||
var osuBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (Gamu) [Insane].osu");
|
||||
var taikoBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (MMzz) [Oni].osu");
|
||||
var catchBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (Deif) [Salad].osu");
|
||||
var maniaBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (ExPew) [Another].osu");
|
||||
|
||||
osuCalculator = new OsuRuleset().CreateDifficultyCalculator(osuBeatmap);
|
||||
taikoCalculator = new TaikoRuleset().CreateDifficultyCalculator(taikoBeatmap);
|
||||
catchCalculator = new CatchRuleset().CreateDifficultyCalculator(catchBeatmap);
|
||||
maniaCalculator = new ManiaRuleset().CreateDifficultyCalculator(maniaBeatmap);
|
||||
}
|
||||
|
||||
private WorkingBeatmap readBeatmap(ZipArchiveReader archiveReader, string beatmapName)
|
||||
{
|
||||
using var beatmapStream = new MemoryStream();
|
||||
archiveReader.GetStream(beatmapName).CopyTo(beatmapStream);
|
||||
|
||||
beatmapStream.Seek(0, SeekOrigin.Begin);
|
||||
using var reader = new LineBufferedReader(beatmapStream);
|
||||
|
||||
var decoder = Beatmaps.Formats.Decoder.GetDecoder<Beatmap>(reader);
|
||||
return new FlatWorkingBeatmap(decoder.Decode(reader));
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void CalculateDifficultyOsu() => osuCalculator.Calculate();
|
||||
|
||||
[Benchmark]
|
||||
public void CalculateDifficultyTaiko() => taikoCalculator.Calculate();
|
||||
|
||||
[Benchmark]
|
||||
public void CalculateDifficultyCatch() => catchCalculator.Calculate();
|
||||
|
||||
[Benchmark]
|
||||
public void CalculateDifficultyMania() => maniaCalculator.Calculate();
|
||||
|
||||
[Benchmark]
|
||||
public void CalculateDifficultyOsuHundredTimes()
|
||||
{
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
osuCalculator.Calculate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
@@ -21,8 +22,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
var ruleset = new CatchRuleset();
|
||||
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
|
||||
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
|
||||
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, []);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
|
||||
}
|
||||
@@ -32,8 +34,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
var ruleset = new CatchRuleset();
|
||||
var difficulty = new BeatmapDifficulty();
|
||||
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
|
||||
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, [new CatchModHalfTime()]);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
|
||||
}
|
||||
@@ -43,8 +46,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
var ruleset = new CatchRuleset();
|
||||
var difficulty = new BeatmapDifficulty();
|
||||
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
|
||||
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, [new CatchModDoubleTime()]);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneCatchModMovingFast : ModTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestMovingFast() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new CatchModMovingFast(),
|
||||
PassCondition = () => true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -1,2 +0,0 @@
|
||||
[General]
|
||||
// no version specified means v1
|
||||
@@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTinyDropletMissPreservesCatcherState()
|
||||
public void TestTinyDropletMissChangesCatcherState()
|
||||
{
|
||||
AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit
|
||||
{
|
||||
@@ -165,8 +165,8 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}));
|
||||
AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet()));
|
||||
AddStep("miss tiny droplet", () => attemptCatch(new TinyDroplet { X = 100 }));
|
||||
// catcher state and hyper dash state is preserved
|
||||
checkState(CatcherAnimationState.Kiai);
|
||||
// catcher state is changed but hyper dash state is preserved
|
||||
checkState(CatcherAnimationState.Fail);
|
||||
checkHyperDash(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@@ -11,6 +12,7 @@ using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Difficulty;
|
||||
using osu.Game.Rulesets.Catch.Edit;
|
||||
@@ -25,6 +27,7 @@ using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
@@ -33,6 +36,7 @@ using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch
|
||||
@@ -150,6 +154,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
new CatchModFloatingFruits(),
|
||||
new CatchModMuted(),
|
||||
new CatchModNoScope(),
|
||||
new CatchModMovingFast(),
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
@@ -265,9 +270,10 @@ namespace osu.Game.Rulesets.Catch
|
||||
}
|
||||
|
||||
/// <seealso cref="CatchHitObject.ApplyDefaultsToSelf"/>
|
||||
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
|
||||
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapInfo beatmapInfo, IReadOnlyCollection<Mod> mods)
|
||||
{
|
||||
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
|
||||
BeatmapDifficulty adjustedDifficulty = base.GetAdjustedDisplayDifficulty(beatmapInfo, mods);
|
||||
double rate = ModUtils.CalculateRateWithMods(mods);
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
|
||||
preempt /= rate;
|
||||
@@ -276,6 +282,33 @@ namespace osu.Game.Rulesets.Catch
|
||||
return adjustedDifficulty;
|
||||
}
|
||||
|
||||
public override IEnumerable<RulesetBeatmapAttribute> GetBeatmapAttributesForDisplay(IBeatmapInfo beatmapInfo, IReadOnlyCollection<Mod> mods)
|
||||
{
|
||||
var originalDifficulty = beatmapInfo.Difficulty;
|
||||
var effectiveDifficulty = GetAdjustedDisplayDifficulty(beatmapInfo, mods);
|
||||
|
||||
yield return new RulesetBeatmapAttribute(SongSelectStrings.CircleSize, @"CS", originalDifficulty.CircleSize, effectiveDifficulty.CircleSize, 10)
|
||||
{
|
||||
Description = "Affects the size of fruits.",
|
||||
AdditionalMetrics =
|
||||
[
|
||||
new RulesetBeatmapAttribute.AdditionalMetric("Hit circle radius", (CatchHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(effectiveDifficulty.CircleSize)).ToLocalisableString("0.#"))
|
||||
]
|
||||
};
|
||||
yield return new RulesetBeatmapAttribute(SongSelectStrings.ApproachRate, @"AR", originalDifficulty.ApproachRate, effectiveDifficulty.ApproachRate, 10)
|
||||
{
|
||||
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"))
|
||||
]
|
||||
};
|
||||
yield return new RulesetBeatmapAttribute(SongSelectStrings.HPDrain, @"HP", originalDifficulty.DrainRate, effectiveDifficulty.DrainRate, 10)
|
||||
{
|
||||
Description = "Affects the harshness of health drain and the health penalties for missing."
|
||||
};
|
||||
}
|
||||
|
||||
public override bool EditorShowScrollSpeed => false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
@@ -51,15 +51,13 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
// Combo scaling
|
||||
if (catchAttributes.MaxCombo > 0)
|
||||
value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0);
|
||||
value *= Math.Min(Math.Pow(score.MaxCombo, 0.35) / Math.Pow(catchAttributes.MaxCombo, 0.35), 1.0);
|
||||
|
||||
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
|
||||
|
||||
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
||||
|
||||
var track = new TrackVirtual(10000);
|
||||
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
double clockRate = track.Rate;
|
||||
double clockRate = ModUtils.CalculateRateWithMods(score.Mods);
|
||||
|
||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Difficulty.Evaluators
|
||||
{
|
||||
public static class MovementEvaluator
|
||||
{
|
||||
private const double direction_change_bonus = 21.0;
|
||||
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, double catcherSpeedMultiplier)
|
||||
{
|
||||
var catchCurrent = (CatchDifficultyHitObject)current;
|
||||
var catchLast = (CatchDifficultyHitObject)current.Previous(0);
|
||||
var catchLastLast = (CatchDifficultyHitObject)current.Previous(1);
|
||||
|
||||
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
|
||||
|
||||
double distanceAddition = (Math.Pow(Math.Abs(catchCurrent.DistanceMoved), 1.3) / 510);
|
||||
double sqrtStrain = Math.Sqrt(weightedStrainTime);
|
||||
|
||||
double edgeDashBonus = 0;
|
||||
|
||||
// Direction change bonus.
|
||||
if (Math.Abs(catchCurrent.DistanceMoved) > 0.1)
|
||||
{
|
||||
if (current.Index >= 1 && Math.Abs(catchLast.DistanceMoved) > 0.1 && Math.Sign(catchCurrent.DistanceMoved) != Math.Sign(catchLast.DistanceMoved))
|
||||
{
|
||||
double bonusFactor = Math.Min(50, Math.Abs(catchCurrent.DistanceMoved)) / 50;
|
||||
double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(catchLast.DistanceMoved)) / 70, 0.38);
|
||||
|
||||
distanceAddition += direction_change_bonus / Math.Sqrt(catchLast.StrainTime + 16) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0);
|
||||
}
|
||||
|
||||
// Base bonus for every movement, giving some weight to streams.
|
||||
distanceAddition += 12.5 * Math.Min(Math.Abs(catchCurrent.DistanceMoved), CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 2)
|
||||
/ (CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 6) / sqrtStrain;
|
||||
}
|
||||
|
||||
// Bonus for edge dashes.
|
||||
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
|
||||
{
|
||||
if (!catchCurrent.LastObject.HyperDash)
|
||||
edgeDashBonus += 5.7;
|
||||
|
||||
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20)
|
||||
* Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
||||
}
|
||||
|
||||
// There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than
|
||||
// the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and NORMALIZED_HALF_CATCHER_WIDTH offsets
|
||||
// We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified.
|
||||
// To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and NORMALIZED_HALF_CATCHER_WIDTH)
|
||||
if (current.Index >= 2 && Math.Abs(catchCurrent.ExactDistanceMoved) <= CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 2
|
||||
&& catchCurrent.ExactDistanceMoved == -catchLast.ExactDistanceMoved && catchLast.ExactDistanceMoved == -catchLastLast.ExactDistanceMoved
|
||||
&& catchCurrent.StrainTime == catchLast.StrainTime && catchLast.StrainTime == catchLastLast.StrainTime)
|
||||
distanceAddition = 0;
|
||||
|
||||
return distanceAddition / weightedStrainTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,15 +11,49 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
|
||||
{
|
||||
public class CatchDifficultyHitObject : DifficultyHitObject
|
||||
{
|
||||
private const float normalized_hitobject_radius = 41.0f;
|
||||
public const float NORMALIZED_HALF_CATCHER_WIDTH = 41.0f;
|
||||
private const float absolute_player_positioning_error = 16.0f;
|
||||
|
||||
public new PalpableCatchHitObject BaseObject => (PalpableCatchHitObject)base.BaseObject;
|
||||
|
||||
public new PalpableCatchHitObject LastObject => (PalpableCatchHitObject)base.LastObject;
|
||||
|
||||
/// <summary>
|
||||
/// Normalized position of <see cref="BaseObject"/>.
|
||||
/// </summary>
|
||||
public readonly float NormalizedPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Normalized position of <see cref="LastObject"/>.
|
||||
/// </summary>
|
||||
public readonly float LastNormalizedPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Normalized position of the player required to catch <see cref="BaseObject"/>, assuming the player moves as little as possible.
|
||||
/// </summary>
|
||||
public float PlayerPosition { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalized position of the player after catching <see cref="LastObject"/>.
|
||||
/// </summary>
|
||||
public float LastPlayerPosition { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalized distance between <see cref="LastPlayerPosition"/> and <see cref="PlayerPosition"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The sign of the value indicates the direction of the movement: negative is left and positive is right.
|
||||
/// </remarks>
|
||||
public float DistanceMoved { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalized distance the player has to move from <see cref="LastPlayerPosition"/> in order to catch <see cref="BaseObject"/> at its <see cref="NormalizedPosition"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The sign of the value indicates the direction of the movement: negative is left and positive is right.
|
||||
/// </remarks>
|
||||
public float ExactDistanceMoved { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Milliseconds elapsed since the start time of the previous <see cref="CatchDifficultyHitObject"/>, with a minimum of 40ms.
|
||||
/// </summary>
|
||||
@@ -29,13 +63,35 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
|
||||
: base(hitObject, lastObject, clockRate, objects, index)
|
||||
{
|
||||
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||
float scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
|
||||
float scalingFactor = NORMALIZED_HALF_CATCHER_WIDTH / halfCatcherWidth;
|
||||
|
||||
NormalizedPosition = BaseObject.EffectiveX * scalingFactor;
|
||||
LastNormalizedPosition = LastObject.EffectiveX * scalingFactor;
|
||||
|
||||
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
|
||||
StrainTime = Math.Max(40, DeltaTime);
|
||||
|
||||
setMovementState();
|
||||
}
|
||||
|
||||
private void setMovementState()
|
||||
{
|
||||
LastPlayerPosition = Index == 0 ? LastNormalizedPosition : ((CatchDifficultyHitObject)Previous(0)).PlayerPosition;
|
||||
|
||||
PlayerPosition = Math.Clamp(
|
||||
LastPlayerPosition,
|
||||
NormalizedPosition - (NORMALIZED_HALF_CATCHER_WIDTH - absolute_player_positioning_error),
|
||||
NormalizedPosition + (NORMALIZED_HALF_CATCHER_WIDTH - absolute_player_positioning_error)
|
||||
);
|
||||
|
||||
DistanceMoved = PlayerPosition - LastPlayerPosition;
|
||||
|
||||
// For the exact position we consider that the catcher is in the correct position for both objects
|
||||
ExactDistanceMoved = NormalizedPosition - LastPlayerPosition;
|
||||
|
||||
// After a hyperdash we ARE in the correct position. Always!
|
||||
if (LastObject.HyperDash)
|
||||
PlayerPosition = NormalizedPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// 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.Catch.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Catch.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -11,10 +10,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
{
|
||||
public class Movement : StrainDecaySkill
|
||||
{
|
||||
private const float absolute_player_positioning_error = 16f;
|
||||
private const float normalized_hitobject_radius = 41.0f;
|
||||
private const double direction_change_bonus = 21.0;
|
||||
|
||||
protected override double SkillMultiplier => 1;
|
||||
protected override double StrainDecayBase => 0.2;
|
||||
|
||||
@@ -24,12 +19,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
|
||||
protected readonly float HalfCatcherWidth;
|
||||
|
||||
private float? lastPlayerPosition;
|
||||
private float lastDistanceMoved;
|
||||
private float lastExactDistanceMoved;
|
||||
private double lastStrainTime;
|
||||
private bool isInBuzzSection;
|
||||
|
||||
/// <summary>
|
||||
/// The speed multiplier applied to the player's catcher.
|
||||
/// </summary>
|
||||
@@ -49,80 +38,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
var catchCurrent = (CatchDifficultyHitObject)current;
|
||||
|
||||
lastPlayerPosition ??= catchCurrent.LastNormalizedPosition;
|
||||
|
||||
float playerPosition = Math.Clamp(
|
||||
lastPlayerPosition.Value,
|
||||
catchCurrent.NormalizedPosition - (normalized_hitobject_radius - absolute_player_positioning_error),
|
||||
catchCurrent.NormalizedPosition + (normalized_hitobject_radius - absolute_player_positioning_error)
|
||||
);
|
||||
|
||||
float distanceMoved = playerPosition - lastPlayerPosition.Value;
|
||||
|
||||
// For the exact position we consider that the catcher is in the correct position for both objects
|
||||
float exactDistanceMoved = catchCurrent.NormalizedPosition - lastPlayerPosition.Value;
|
||||
|
||||
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
|
||||
|
||||
double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
|
||||
double sqrtStrain = Math.Sqrt(weightedStrainTime);
|
||||
|
||||
double edgeDashBonus = 0;
|
||||
|
||||
// Direction change bonus.
|
||||
if (Math.Abs(distanceMoved) > 0.1)
|
||||
{
|
||||
if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved))
|
||||
{
|
||||
double bonusFactor = Math.Min(50, Math.Abs(distanceMoved)) / 50;
|
||||
double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(lastDistanceMoved)) / 70, 0.38);
|
||||
|
||||
distanceAddition += direction_change_bonus / Math.Sqrt(lastStrainTime + 16) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0);
|
||||
}
|
||||
|
||||
// Base bonus for every movement, giving some weight to streams.
|
||||
distanceAddition += 12.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain;
|
||||
}
|
||||
|
||||
// Bonus for edge dashes.
|
||||
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
|
||||
{
|
||||
if (!catchCurrent.LastObject.HyperDash)
|
||||
edgeDashBonus += 5.7;
|
||||
else
|
||||
{
|
||||
// After a hyperdash we ARE in the correct position. Always!
|
||||
playerPosition = catchCurrent.NormalizedPosition;
|
||||
}
|
||||
|
||||
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20)
|
||||
* Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
||||
}
|
||||
|
||||
// There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than
|
||||
// the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and normalized_hitobject_radius offsets
|
||||
// We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified.
|
||||
// To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius)
|
||||
if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime)
|
||||
{
|
||||
if (isInBuzzSection)
|
||||
distanceAddition = 0;
|
||||
else
|
||||
isInBuzzSection = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isInBuzzSection = false;
|
||||
}
|
||||
|
||||
lastPlayerPosition = playerPosition;
|
||||
lastDistanceMoved = distanceMoved;
|
||||
lastStrainTime = catchCurrent.StrainTime;
|
||||
lastExactDistanceMoved = exactDistanceMoved;
|
||||
|
||||
return distanceAddition / weightedStrainTime;
|
||||
return MovementEvaluator.EvaluateDifficultyOf(current, catcherSpeedMultiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
new CheckBananaShowerGap(),
|
||||
new CheckConcurrentObjects(),
|
||||
|
||||
// Spread
|
||||
new CheckCatchLowestDiffDrainTime(),
|
||||
|
||||
// Settings
|
||||
new CheckCatchAbnormalDifficultySettings(),
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Checks
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
var hitObjects = context.Beatmap.HitObjects;
|
||||
var hitObjects = context.CurrentDifficulty.Playable.HitObjects;
|
||||
(int expectedStartDelta, int expectedEndDelta) = spinner_delta_threshold[context.InterpretedDifficulty];
|
||||
|
||||
for (int i = 0; i < hitObjects.Count - 1; ++i)
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Checks
|
||||
|
||||
public override IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
var diff = context.Beatmap.Difficulty;
|
||||
var diff = context.CurrentDifficulty.Playable.Difficulty;
|
||||
Issue? issue;
|
||||
|
||||
if (HasMoreThanOneDecimalPlace("Approach rate", diff.ApproachRate, out issue))
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Checks
|
||||
{
|
||||
public class CheckCatchLowestDiffDrainTime : CheckLowestDiffDrainTime
|
||||
{
|
||||
protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds()
|
||||
{
|
||||
// See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21catch#general
|
||||
yield return (DifficultyRating.Hard, new TimeSpan(0, 2, 30).TotalMilliseconds, "Platter");
|
||||
yield return (DifficultyRating.Insane, new TimeSpan(0, 3, 15).TotalMilliseconds, "Rain");
|
||||
yield return (DifficultyRating.Expert, new TimeSpan(0, 4, 0).TotalMilliseconds, "Overdose");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -10,6 +12,8 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModAutoplay : ModAutoplay
|
||||
{
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(CatchModMovingFast) }).ToArray();
|
||||
|
||||
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
=> new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "osu!salad" });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
@@ -11,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModCinema : ModCinema<CatchHitObject>
|
||||
{
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(CatchModMovingFast) }).ToArray();
|
||||
|
||||
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
=> new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "osu!salad" });
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
|
||||
public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
|
||||
|
||||
public override float DefaultFlashlightSize => 325;
|
||||
public override float DefaultFlashlightSize => 203.125f;
|
||||
|
||||
protected override Flashlight CreateFlashlight() => new CatchFlashlight(this, playfield);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
public override string Acronym => "FF";
|
||||
public override LocalisableString Description => "The fruits are... floating?";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Cloud;
|
||||
public override IconUsage? Icon => OsuIcon.ModFloatingFruits;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public partial class CatchModMovingFast : Mod, IApplicableToDrawableRuleset<CatchHitObject>, IApplicableToPlayer
|
||||
{
|
||||
public override string Name => "Moving Fast";
|
||||
public override string Acronym => "MF";
|
||||
public override LocalisableString Description => "Dashing by default, slow down!";
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override IconUsage? Icon => OsuIcon.ModMovingFast;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) };
|
||||
|
||||
private DrawableCatchRuleset drawableRuleset = null!;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
||||
{
|
||||
this.drawableRuleset = (DrawableCatchRuleset)drawableRuleset;
|
||||
}
|
||||
|
||||
public void ApplyToPlayer(Player player)
|
||||
{
|
||||
if (!drawableRuleset.HasReplayLoaded.Value)
|
||||
{
|
||||
var catchPlayfield = (CatchPlayfield)drawableRuleset.Playfield;
|
||||
catchPlayfield.Catcher.Dashing = true;
|
||||
catchPlayfield.CatcherArea.Add(new InvertDashInputHelper(catchPlayfield.CatcherArea));
|
||||
}
|
||||
}
|
||||
|
||||
private partial class InvertDashInputHelper : Drawable, IKeyBindingHandler<CatchAction>
|
||||
{
|
||||
private readonly CatcherArea catcherArea;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
|
||||
public InvertDashInputHelper(CatcherArea catcherArea)
|
||||
{
|
||||
this.catcherArea = catcherArea;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<CatchAction> e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case CatchAction.MoveLeft or CatchAction.MoveRight:
|
||||
break;
|
||||
|
||||
case CatchAction.Dash:
|
||||
catcherArea.Catcher.Dashing = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<CatchAction> e)
|
||||
{
|
||||
if (e.Action == CatchAction.Dash)
|
||||
catcherArea.Catcher.Dashing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
@@ -19,6 +21,8 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public override LocalisableString Description => @"Use the mouse to control the catcher.";
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(CatchModMovingFast) }).ToArray();
|
||||
|
||||
private DrawableCatchRuleset drawableRuleset = null!;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
||||
|
||||
@@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN);
|
||||
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_RANGE);
|
||||
|
||||
Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize);
|
||||
}
|
||||
@@ -203,6 +203,8 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
/// </summary>
|
||||
public const double PREEMPT_MAX = 1800;
|
||||
|
||||
public static readonly DifficultyRange PREEMPT_RANGE = new DifficultyRange(PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN);
|
||||
|
||||
/// <summary>
|
||||
/// The Y position of the hit object is not used in the normal osu!catch gameplay.
|
||||
/// It is preserved to maximize the backward compatibility with the legacy editor, in which the mappers use the Y position to organize the patterns.
|
||||
|
||||
@@ -224,7 +224,20 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
addLighting(result, drawableObject.AccentColour.Value, positionInStack.X);
|
||||
}
|
||||
|
||||
// droplet doesn't affect the catcher state
|
||||
if (result.IsHit)
|
||||
CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
|
||||
else if (hitObject is not Banana)
|
||||
CurrentState = CatcherAnimationState.Fail;
|
||||
|
||||
if (palpableObject.HitObject.LastInCombo)
|
||||
{
|
||||
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
|
||||
Explode();
|
||||
else
|
||||
Drop();
|
||||
}
|
||||
|
||||
// droplet doesn't affect hyperdash state
|
||||
if (hitObject is TinyDroplet) return;
|
||||
|
||||
// if a hyper fruit was already handled this frame, just go where it says to go.
|
||||
@@ -244,19 +257,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
else
|
||||
SetHyperDashState();
|
||||
}
|
||||
|
||||
if (result.IsHit)
|
||||
CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
|
||||
else if (!(hitObject is Banana))
|
||||
CurrentState = CatcherAnimationState.Fail;
|
||||
|
||||
if (palpableObject.HitObject.LastInCombo)
|
||||
{
|
||||
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
|
||||
Explode();
|
||||
else
|
||||
Drop();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnRevertResult(JudgementResult result)
|
||||
|
||||
@@ -55,6 +55,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHoldNotesAlmostConcurrentOnSameColumn()
|
||||
{
|
||||
assertAlmostConcurrentSame(new List<HitObject>
|
||||
{
|
||||
createHoldNote(startTime: 100, endTime: 400.75d, column: 1),
|
||||
createHoldNote(startTime: 408, endTime: 700.75d, column: 1)
|
||||
});
|
||||
}
|
||||
|
||||
private void assertOk(List<HitObject> hitobjects)
|
||||
{
|
||||
Assert.That(check.Run(getContext(hitobjects)), Is.Empty);
|
||||
@@ -65,7 +75,17 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks
|
||||
var issues = check.Run(getContext(hitobjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentSame));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrent));
|
||||
Assert.That(issues.All(issue => issue.ToString().Contains("s are concurrent here")));
|
||||
}
|
||||
|
||||
private void assertAlmostConcurrentSame(List<HitObject> hitobjects)
|
||||
{
|
||||
var issues = check.Run(getContext(hitobjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateAlmostConcurrent));
|
||||
Assert.That(issues.All(issue => issue.ToString().Contains("s are less than 10ms apart")));
|
||||
}
|
||||
|
||||
private BeatmapVerifierContext getContext(List<HitObject> hitobjects)
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
[TestCase("convert-samples")]
|
||||
[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")]
|
||||
public void Test(string name) => base.Test(name);
|
||||
|
||||
@@ -32,6 +33,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
StartTime = hitObject.StartTime,
|
||||
EndTime = hitObject.GetEndTime(),
|
||||
Column = ((ManiaHitObject)hitObject).Column,
|
||||
PlaySlidingSamples = hitObject is HoldNote holdNote && holdNote.PlaySlidingSamples,
|
||||
Samples = getSampleNames(hitObject.Samples),
|
||||
NodeSamples = getNodeSampleNames((hitObject as HoldNote)?.NodeSamples)
|
||||
};
|
||||
@@ -57,12 +59,14 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
public double StartTime;
|
||||
public double EndTime;
|
||||
public int Column;
|
||||
public bool PlaySlidingSamples;
|
||||
public IList<string> Samples;
|
||||
public IList<IList<string>> NodeSamples;
|
||||
|
||||
public bool Equals(SampleConvertValue other)
|
||||
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
|
||||
&& Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
|
||||
&& PlaySlidingSamples == other.PlaySlidingSamples
|
||||
&& samplesEqual(Samples, other.Samples)
|
||||
&& nodeSamplesEqual(NodeSamples, other.NodeSamples);
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestFilterIntersection()
|
||||
public void TestKeysFilterIntersection()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.Greater, "4");
|
||||
@@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestInvalidFilters()
|
||||
public void TestInvalidKeysFilters()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
|
||||
@@ -183,5 +183,132 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "4,some text"));
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4,5,6"));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestLnsEqual()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
var filterCriteria = new FilterCriteria
|
||||
{
|
||||
Ruleset = new ManiaRuleset().RulesetInfo
|
||||
};
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0");
|
||||
BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 0,
|
||||
EndTimeObjectCount = 0
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo1, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0");
|
||||
BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 0
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo2, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "100");
|
||||
BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 100
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo3, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "1");
|
||||
BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 1
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo4, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0.1");
|
||||
BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 1000,
|
||||
EndTimeObjectCount = 1
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo5, filterCriteria));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestLnsGreaterOrEqual()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
var filterCriteria = new FilterCriteria
|
||||
{
|
||||
Ruleset = new ManiaRuleset().RulesetInfo
|
||||
};
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0");
|
||||
BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 0,
|
||||
EndTimeObjectCount = 0
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo1, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0");
|
||||
BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 0
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo2, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "100");
|
||||
BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 100
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo3, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1");
|
||||
BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 1
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo4, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0.1");
|
||||
BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 1000,
|
||||
EndTimeObjectCount = 1
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo5, filterCriteria));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestLnsNotManiaRuleset()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
var filterCriteria = new FilterCriteria
|
||||
{
|
||||
Ruleset = new ManiaRuleset().RulesetInfo
|
||||
};
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.LessOrEqual, "100");
|
||||
BeatmapInfo beatmapInfo = new BeatmapInfo
|
||||
{
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 50
|
||||
};
|
||||
Assert.False(criteria.Matches(beatmapInfo, filterCriteria));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestInvalidLnsFilters()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "some text"));
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1some text"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"StartTime": 1000.0,
|
||||
"EndTime": 2750.0,
|
||||
"Column": 1,
|
||||
"PlaySlidingSamples": true,
|
||||
"NodeSamples": [
|
||||
["Gameplay/normal-hitnormal"],
|
||||
["Gameplay/soft-hitnormal"],
|
||||
@@ -15,6 +16,7 @@
|
||||
"StartTime": 1875.0,
|
||||
"EndTime": 2750.0,
|
||||
"Column": 0,
|
||||
"PlaySlidingSamples": true,
|
||||
"NodeSamples": [
|
||||
["Gameplay/soft-hitnormal"],
|
||||
["Gameplay/drum-hitnormal"]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"StartTime": 500.0,
|
||||
"EndTime": 1500.0,
|
||||
"Column": 0,
|
||||
"PlaySlidingSamples": false,
|
||||
"NodeSamples": [
|
||||
["Gameplay/normal-hitnormal"],
|
||||
[]
|
||||
@@ -17,6 +18,7 @@
|
||||
"StartTime": 2000.0,
|
||||
"EndTime": 3000.0,
|
||||
"Column": 2,
|
||||
"PlaySlidingSamples": false,
|
||||
"NodeSamples": [
|
||||
["Gameplay/drum-hitnormal"],
|
||||
[]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"Mappings": [{
|
||||
"StartTime": 500.0,
|
||||
"Objects": [{
|
||||
"StartTime": 500.0,
|
||||
"EndTime": 2500,
|
||||
"Column": 2,
|
||||
"PlaySlidingSamples": true,
|
||||
"NodeSamples": [
|
||||
["Gameplay/soft-hitnormal"],
|
||||
["Gameplay/soft-hitnormal"],
|
||||
["Gameplay/soft-hitnormal"],
|
||||
["Gameplay/soft-hitnormal"]
|
||||
],
|
||||
"Samples": ["Gameplay/soft-hitnormal"]
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
osu file format v5
|
||||
|
||||
[General]
|
||||
StackLeniency: 0.7
|
||||
Mode: 3
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:2
|
||||
CircleSize:5
|
||||
OverallDifficulty:2
|
||||
SliderMultiplier:1
|
||||
SliderTickRate:2
|
||||
|
||||
[Events]
|
||||
//Background and Video events
|
||||
//Break Periods
|
||||
//Storyboard Layer 0 (Background)
|
||||
//Storyboard Layer 1 (Failing)
|
||||
//Storyboard Layer 2 (Passing)
|
||||
//Storyboard Layer 3 (Foreground)
|
||||
//Storyboard Sound Samples
|
||||
//Background Colour Transformations
|
||||
3,100,163,162,255
|
||||
|
||||
[TimingPoints]
|
||||
355,476.190476190476,4,2,1,60,1,0
|
||||
|
||||
[HitObjects]
|
||||
256,352,500,2,0,L|256:208,3,140
|
||||
@@ -203,7 +203,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
private void toggleTouchControls(bool enabled)
|
||||
{
|
||||
var maniaConfig = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(CreatePlayerRuleset())!;
|
||||
maniaConfig.SetValue(ManiaRulesetSetting.MobileLayout, enabled ? ManiaMobileLayout.LandscapeWithOverlay : ManiaMobileLayout.Portrait);
|
||||
maniaConfig.SetValue(ManiaRulesetSetting.TouchOverlay, enabled);
|
||||
}
|
||||
|
||||
private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType<ManiaTouchInputArea>().SingleOrDefault();
|
||||
|
||||
@@ -47,8 +47,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
|
||||
}
|
||||
};
|
||||
|
||||
drawableRuleset.AllowBackwardsSeeks = true;
|
||||
});
|
||||
AddStep("retrieve config bindable", () =>
|
||||
{
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty, IReadOnlyList<Mod>? mods = null)
|
||||
public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty, IReadOnlyCollection<Mod>? mods = null)
|
||||
{
|
||||
var converter = new ManiaBeatmapConverter(null, difficulty, new ManiaRuleset());
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Utils;
|
||||
|
||||
@@ -30,12 +32,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
|
||||
if (HitObject is IHasDuration endTimeData)
|
||||
{
|
||||
// despite the beatmap originally being made for mania, if the object is parsed as a slider rather than a hold, sliding samples should still be played.
|
||||
// this is seemingly only possible to achieve by modifying the .osu file directly, but online beatmaps that do that exist
|
||||
// (see second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407)
|
||||
bool playSlidingSamples = (HitObject is IHasLegacyHitObjectType hasType && hasType.LegacyType == LegacyHitObjectType.Slider) || HitObject is IHasPath;
|
||||
|
||||
pattern.Add(new HoldNote
|
||||
{
|
||||
StartTime = HitObject.StartTime,
|
||||
Duration = endTimeData.Duration,
|
||||
Column = column,
|
||||
Samples = HitObject.Samples,
|
||||
PlaySlidingSamples = playSlidingSamples,
|
||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? HoldNote.CreateDefaultNodeSamples(HitObject)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -521,6 +521,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
Duration = endTime - startTime,
|
||||
Column = column,
|
||||
Samples = HitObject.Samples,
|
||||
PlaySlidingSamples = true,
|
||||
NodeSamples = nodeSamplesAt(startTime)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
public ManiaRulesetConfigManager(SettingsStore? settings, RulesetInfo ruleset, int? variant = null)
|
||||
: base(settings, ruleset, variant)
|
||||
{
|
||||
Migrate();
|
||||
}
|
||||
|
||||
protected override void InitialiseDefaults()
|
||||
@@ -24,6 +25,20 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
|
||||
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
|
||||
SetDefault(ManiaRulesetSetting.MobileLayout, ManiaMobileLayout.Portrait);
|
||||
SetDefault(ManiaRulesetSetting.TouchOverlay, false);
|
||||
}
|
||||
|
||||
public void Migrate()
|
||||
{
|
||||
var mobileLayout = GetBindable<ManiaMobileLayout>(ManiaRulesetSetting.MobileLayout);
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (mobileLayout.Value == ManiaMobileLayout.LandscapeWithOverlay)
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
mobileLayout.Value = ManiaMobileLayout.Landscape;
|
||||
SetValue(ManiaRulesetSetting.TouchOverlay, true);
|
||||
}
|
||||
}
|
||||
|
||||
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
|
||||
@@ -44,5 +59,6 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
ScrollDirection,
|
||||
TimingBasedNoteColouring,
|
||||
MobileLayout,
|
||||
TouchOverlay,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Difficulty.Evaluators
|
||||
{
|
||||
public class IndividualStrainEvaluator
|
||||
{
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||
{
|
||||
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||
double startTime = maniaCurrent.StartTime;
|
||||
double endTime = maniaCurrent.EndTime;
|
||||
|
||||
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
|
||||
|
||||
// We award a bonus if this note starts and ends before the end of another hold note.
|
||||
foreach (var maniaPrevious in maniaCurrent.PreviousHitObjects)
|
||||
{
|
||||
if (maniaPrevious is null)
|
||||
continue;
|
||||
|
||||
if (Precision.DefinitelyBigger(maniaPrevious.EndTime, endTime, 1) &&
|
||||
Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1))
|
||||
{
|
||||
holdFactor = 1.25;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 2.0 * holdFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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.Utils;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Difficulty.Evaluators
|
||||
{
|
||||
public class OverallStrainEvaluator
|
||||
{
|
||||
private const double release_threshold = 30;
|
||||
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||
{
|
||||
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||
double startTime = maniaCurrent.StartTime;
|
||||
double endTime = maniaCurrent.EndTime;
|
||||
bool isOverlapping = false;
|
||||
|
||||
double closestEndTime = Math.Abs(endTime - startTime); // Lowest value we can assume with the current information
|
||||
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
|
||||
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
|
||||
|
||||
foreach (var maniaPrevious in maniaCurrent.PreviousHitObjects)
|
||||
{
|
||||
if (maniaPrevious is null)
|
||||
continue;
|
||||
|
||||
// The current note is overlapped if a previous note or end is overlapping the current note body
|
||||
isOverlapping |= Precision.DefinitelyBigger(maniaPrevious.EndTime, startTime, 1) &&
|
||||
Precision.DefinitelyBigger(endTime, maniaPrevious.EndTime, 1) &&
|
||||
Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1);
|
||||
|
||||
// We give a slight bonus to everything if something is held meanwhile
|
||||
if (Precision.DefinitelyBigger(maniaPrevious.EndTime, endTime, 1) &&
|
||||
Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1))
|
||||
holdFactor = 1.25;
|
||||
|
||||
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - maniaPrevious.EndTime));
|
||||
}
|
||||
|
||||
// The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
|
||||
// Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
|
||||
// holdAddition
|
||||
// ^
|
||||
// 1.0 + - - - - - -+-----------
|
||||
// | /
|
||||
// 0.5 + - - - - -/ Sigmoid Curve
|
||||
// | /|
|
||||
// 0.0 +--------+-+---------------> Release Difference / ms
|
||||
// release_threshold
|
||||
if (isOverlapping)
|
||||
holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold);
|
||||
|
||||
return (1 + holdAddition) * holdFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,13 +65,22 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||
{
|
||||
var sortedObjects = beatmap.HitObjects.ToArray();
|
||||
int totalColumns = ((ManiaBeatmap)beatmap).TotalColumns;
|
||||
|
||||
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>();
|
||||
List<DifficultyHitObject>[] perColumnObjects = new List<DifficultyHitObject>[totalColumns];
|
||||
|
||||
for (int column = 0; column < totalColumns; column++)
|
||||
perColumnObjects[column] = new List<DifficultyHitObject>();
|
||||
|
||||
for (int i = 1; i < sortedObjects.Length; i++)
|
||||
objects.Add(new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate, objects, objects.Count));
|
||||
{
|
||||
var currentObject = new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate, objects, perColumnObjects, objects.Count);
|
||||
objects.Add(currentObject);
|
||||
perColumnObjects[currentObject.Column].Add(currentObject);
|
||||
}
|
||||
|
||||
return objects;
|
||||
}
|
||||
|
||||
@@ -12,9 +12,59 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Preprocessing
|
||||
{
|
||||
public new ManiaHitObject BaseObject => (ManiaHitObject)base.BaseObject;
|
||||
|
||||
public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List<DifficultyHitObject> objects, int index)
|
||||
private readonly List<DifficultyHitObject>[] perColumnObjects;
|
||||
|
||||
private readonly int columnIndex;
|
||||
|
||||
public readonly int Column;
|
||||
|
||||
// The hit object earlier in time than this note in each column
|
||||
public readonly ManiaDifficultyHitObject?[] PreviousHitObjects;
|
||||
|
||||
public readonly double ColumnStrainTime;
|
||||
|
||||
public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List<DifficultyHitObject> objects, List<DifficultyHitObject>[] perColumnObjects, int index)
|
||||
: base(hitObject, lastObject, clockRate, objects, index)
|
||||
{
|
||||
int totalColumns = perColumnObjects.Length;
|
||||
this.perColumnObjects = perColumnObjects;
|
||||
Column = BaseObject.Column;
|
||||
columnIndex = perColumnObjects[Column].Count;
|
||||
PreviousHitObjects = new ManiaDifficultyHitObject[totalColumns];
|
||||
ColumnStrainTime = StartTime - PrevInColumn(0)?.StartTime ?? StartTime;
|
||||
|
||||
if (index > 0)
|
||||
{
|
||||
ManiaDifficultyHitObject prevNote = (ManiaDifficultyHitObject)objects[index - 1];
|
||||
|
||||
for (int i = 0; i < prevNote.PreviousHitObjects.Length; i++)
|
||||
PreviousHitObjects[i] = prevNote.PreviousHitObjects[i];
|
||||
|
||||
// intentionally depends on processing order to match live.
|
||||
PreviousHitObjects[prevNote.Column] = prevNote;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The previous object in the same column as this <see cref="ManiaDifficultyHitObject"/>, exclusive of Long Note tails.
|
||||
/// </summary>
|
||||
/// <param name="backwardsIndex">The number of notes to go back.</param>
|
||||
/// <returns>The object in this column <paramref name="backwardsIndex"/> notes back, or null if this is the first note in the column.</returns>
|
||||
public ManiaDifficultyHitObject? PrevInColumn(int backwardsIndex)
|
||||
{
|
||||
int index = columnIndex - (backwardsIndex + 1);
|
||||
return index >= 0 && index < perColumnObjects[Column].Count ? (ManiaDifficultyHitObject)perColumnObjects[Column][index] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The next object in the same column as this <see cref="ManiaDifficultyHitObject"/>, exclusive of Long Note tails.
|
||||
/// </summary>
|
||||
/// <param name="forwardsIndex">The number of notes to go forward.</param>
|
||||
/// <returns>The object in this column <paramref name="forwardsIndex"/> notes forward, or null if this is the last note in the column.</returns>
|
||||
public ManiaDifficultyHitObject? NextInColumn(int forwardsIndex)
|
||||
{
|
||||
int index = columnIndex + (forwardsIndex + 1);
|
||||
return index >= 0 && index < perColumnObjects[Column].Count ? (ManiaDifficultyHitObject)perColumnObjects[Column][index] : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
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.Mania.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@@ -15,23 +14,17 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
{
|
||||
private const double individual_decay_base = 0.125;
|
||||
private const double overall_decay_base = 0.30;
|
||||
private const double release_threshold = 30;
|
||||
|
||||
protected override double SkillMultiplier => 1;
|
||||
protected override double StrainDecayBase => 1;
|
||||
|
||||
private readonly double[] startTimes;
|
||||
private readonly double[] endTimes;
|
||||
private readonly double[] individualStrains;
|
||||
|
||||
private double individualStrain;
|
||||
private double highestIndividualStrain;
|
||||
private double overallStrain;
|
||||
|
||||
public Strain(Mod[] mods, int totalColumns)
|
||||
: base(mods)
|
||||
{
|
||||
startTimes = new double[totalColumns];
|
||||
endTimes = new double[totalColumns];
|
||||
individualStrains = new double[totalColumns];
|
||||
overallStrain = 1;
|
||||
}
|
||||
@@ -39,65 +32,24 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||
double startTime = maniaCurrent.StartTime;
|
||||
double endTime = maniaCurrent.EndTime;
|
||||
int column = maniaCurrent.BaseObject.Column;
|
||||
bool isOverlapping = false;
|
||||
|
||||
double closestEndTime = Math.Abs(endTime - startTime); // Lowest value we can assume with the current information
|
||||
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
|
||||
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
|
||||
individualStrains[maniaCurrent.Column] = applyDecay(individualStrains[maniaCurrent.Column], maniaCurrent.ColumnStrainTime, individual_decay_base);
|
||||
individualStrains[maniaCurrent.Column] += IndividualStrainEvaluator.EvaluateDifficultyOf(current);
|
||||
|
||||
for (int i = 0; i < endTimes.Length; ++i)
|
||||
{
|
||||
// The current note is overlapped if a previous note or end is overlapping the current note body
|
||||
isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) &&
|
||||
Precision.DefinitelyBigger(endTime, endTimes[i], 1) &&
|
||||
Precision.DefinitelyBigger(startTime, startTimes[i], 1);
|
||||
// Take the hardest individualStrain for notes that happen at the same time (in a chord).
|
||||
// This is to ensure the order in which the notes are processed does not affect the resultant total strain.
|
||||
highestIndividualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(highestIndividualStrain, individualStrains[maniaCurrent.Column]) : individualStrains[maniaCurrent.Column];
|
||||
|
||||
// We give a slight bonus to everything if something is held meanwhile
|
||||
if (Precision.DefinitelyBigger(endTimes[i], endTime, 1) &&
|
||||
Precision.DefinitelyBigger(startTime, startTimes[i], 1))
|
||||
holdFactor = 1.25;
|
||||
|
||||
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i]));
|
||||
}
|
||||
|
||||
// The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
|
||||
// Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
|
||||
// holdAddition
|
||||
// ^
|
||||
// 1.0 + - - - - - -+-----------
|
||||
// | /
|
||||
// 0.5 + - - - - -/ Sigmoid Curve
|
||||
// | /|
|
||||
// 0.0 +--------+-+---------------> Release Difference / ms
|
||||
// release_threshold
|
||||
if (isOverlapping)
|
||||
holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold);
|
||||
|
||||
// Decay and increase individualStrains in own column
|
||||
individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base);
|
||||
individualStrains[column] += 2.0 * holdFactor;
|
||||
|
||||
// For notes at the same time (in a chord), the individualStrain should be the hardest individualStrain out of those columns
|
||||
individualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(individualStrain, individualStrains[column]) : individualStrains[column];
|
||||
|
||||
// Decay and increase overallStrain
|
||||
overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base);
|
||||
overallStrain += (1 + holdAddition) * holdFactor;
|
||||
|
||||
// Update startTimes and endTimes arrays
|
||||
startTimes[column] = startTime;
|
||||
endTimes[column] = endTime;
|
||||
overallStrain = applyDecay(overallStrain, maniaCurrent.DeltaTime, overall_decay_base);
|
||||
overallStrain += OverallStrainEvaluator.EvaluateDifficultyOf(current);
|
||||
|
||||
// By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section.
|
||||
return individualStrain + overallStrain - CurrentStrain;
|
||||
return highestIndividualStrain + overallStrain - CurrentStrain;
|
||||
}
|
||||
|
||||
protected override double CalculateInitialStrain(double offset, DifficultyHitObject current)
|
||||
=> applyDecay(individualStrain, offset - current.Previous(0).StartTime, individual_decay_base)
|
||||
+ applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base);
|
||||
protected override double CalculateInitialStrain(double offset, DifficultyHitObject current) =>
|
||||
applyDecay(highestIndividualStrain, offset - current.Previous(0).StartTime, individual_decay_base)
|
||||
+ applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base);
|
||||
|
||||
private double applyDecay(double value, double deltaTime, double decayBase)
|
||||
=> value * Math.Pow(decayBase, deltaTime / 1000);
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Checks
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
var diff = context.Beatmap.Difficulty;
|
||||
var diff = context.CurrentDifficulty.Playable.Difficulty;
|
||||
|
||||
if (diff.CircleSize < 4)
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Checks
|
||||
|
||||
public override IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
var diff = context.Beatmap.Difficulty;
|
||||
var diff = context.CurrentDifficulty.Playable.Difficulty;
|
||||
Issue? issue;
|
||||
|
||||
if (HasMoreThanOneDecimalPlace("Overall difficulty", diff.OverallDifficulty, out issue))
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Checks
|
||||
{
|
||||
public override IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
var hitObjects = context.Beatmap.HitObjects;
|
||||
var hitObjects = context.CurrentDifficulty.Playable.HitObjects;
|
||||
|
||||
for (int i = 0; i < hitObjects.Count - 1; ++i)
|
||||
{
|
||||
@@ -28,14 +28,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Checks
|
||||
continue;
|
||||
|
||||
// Two hitobjects cannot be concurrent without also being concurrent with all objects in between.
|
||||
// So if the next object is not concurrent, then we know no future objects will be either.
|
||||
if (!AreConcurrent(hitobject, nextHitobject))
|
||||
// So if the next object is not concurrent or almost concurrent, then we know no future objects will be either.
|
||||
if (!AreConcurrent(hitobject, nextHitobject) && !AreAlmostConcurrent(hitobject, nextHitobject))
|
||||
break;
|
||||
|
||||
if (hitobject.GetType() == nextHitobject.GetType())
|
||||
yield return new IssueTemplateConcurrentSame(this).Create(hitobject, nextHitobject);
|
||||
else
|
||||
yield return new IssueTemplateConcurrentDifferent(this).Create(hitobject, nextHitobject);
|
||||
if (AreConcurrent(hitobject, nextHitobject))
|
||||
{
|
||||
yield return new IssueTemplateConcurrent(this).Create(hitobject, nextHitobject);
|
||||
}
|
||||
else if (AreAlmostConcurrent(hitobject, nextHitobject))
|
||||
{
|
||||
yield return new IssueTemplateAlmostConcurrent(this).Create(hitobject, nextHitobject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Checks
|
||||
{
|
||||
public class CheckManiaLowestDiffDrainTime : CheckLowestDiffDrainTime
|
||||
{
|
||||
protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds()
|
||||
{
|
||||
// See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21mania#rules
|
||||
yield return (DifficultyRating.Hard, new TimeSpan(0, 2, 30).TotalMilliseconds, "Hard");
|
||||
yield return (DifficultyRating.Insane, new TimeSpan(0, 2, 45).TotalMilliseconds, "Insane");
|
||||
yield return (DifficultyRating.Expert, new TimeSpan(0, 3, 30).TotalMilliseconds, "Expert");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,9 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
// Compose
|
||||
new CheckManiaConcurrentObjects(),
|
||||
|
||||
// Spread
|
||||
new CheckManiaLowestDiffDrainTime(),
|
||||
|
||||
// Settings
|
||||
new CheckKeyCount(),
|
||||
new CheckManiaAbnormalDifficultySettings(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
@@ -19,12 +20,16 @@ namespace osu.Game.Rulesets.Mania
|
||||
public class ManiaFilterCriteria : IRulesetFilterCriteria
|
||||
{
|
||||
private readonly HashSet<int> includedKeyCounts = Enumerable.Range(1, LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT).ToHashSet();
|
||||
private FilterCriteria.OptionalRange<float> longNotePercentage;
|
||||
|
||||
public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria)
|
||||
{
|
||||
int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods);
|
||||
|
||||
return includedKeyCounts.Contains(keyCount);
|
||||
bool keyCountMatch = includedKeyCounts.Contains(keyCount);
|
||||
bool longNotePercentageMatch = !longNotePercentage.HasFilter || (!isConvertedBeatmap(beatmapInfo) && longNotePercentage.IsInRange(calculateLongNotePercentage(beatmapInfo)));
|
||||
|
||||
return keyCountMatch && longNotePercentageMatch;
|
||||
}
|
||||
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string strValues)
|
||||
@@ -84,6 +89,10 @@ namespace osu.Game.Rulesets.Mania
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
case "ln":
|
||||
case "lns":
|
||||
return FilterQueryParser.TryUpdateCriteriaRange(ref longNotePercentage, op, strValues);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -103,5 +112,18 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool isConvertedBeatmap(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
return !beatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
|
||||
}
|
||||
|
||||
private static float calculateLongNotePercentage(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
int holdNotes = beatmapInfo.EndTimeObjectCount;
|
||||
int totalNotes = Math.Max(1, beatmapInfo.TotalObjectCount);
|
||||
|
||||
return holdNotes / (float)totalNotes * 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
// 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.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
public enum ManiaMobileLayout
|
||||
{
|
||||
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.PortraitExpandedColumns))]
|
||||
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.Portrait))]
|
||||
Portrait,
|
||||
|
||||
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.LandscapeExpandedColumns))]
|
||||
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.Landscape))]
|
||||
Landscape,
|
||||
|
||||
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.LandscapeTouchOverlay))]
|
||||
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.LandscapeExpandedColumns))]
|
||||
LandscapeExpandedColumns,
|
||||
|
||||
[Obsolete($"Use {nameof(ManiaRulesetSetting.TouchOverlay)} instead.")] // todo: can be removed 20260211
|
||||
LandscapeWithOverlay,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
@@ -12,6 +13,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Configuration;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
@@ -77,6 +79,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
return new ManiaArgonSkinTransformer(skin, beatmap);
|
||||
|
||||
case DefaultLegacySkin:
|
||||
case RetroSkin:
|
||||
return new ManiaClassicSkinTransformer(skin, beatmap);
|
||||
|
||||
case LegacySkin:
|
||||
@@ -414,6 +417,70 @@ namespace osu.Game.Rulesets.Mania
|
||||
}), true)
|
||||
};
|
||||
|
||||
/// <seealso cref="ManiaHitWindows"/>
|
||||
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapInfo beatmapInfo, IReadOnlyCollection<Mod> mods)
|
||||
{
|
||||
BeatmapDifficulty adjustedDifficulty = base.GetAdjustedDisplayDifficulty(beatmapInfo, mods);
|
||||
|
||||
// notably, in mania, hit windows are designed to be independent of track playback rate (see `ManiaHitWindows.SpeedMultiplier`).
|
||||
// *however*, to not make matters *too* simple, mania Hard Rock and Easy differ from all other rulesets
|
||||
// in that they apply multipliers *to hit window durations directly* rather than to the Overall Difficulty attribute itself.
|
||||
// because the duration of hit window durations as a function of OD is not a linear function,
|
||||
// this means that multiplying the OD is *not* the same thing as multiplying the hit window duration.
|
||||
// in fact, the second operation is *much* harsher and will produce values much farther outside of normal operating range
|
||||
// (even negative in the case of Easy).
|
||||
// stable handles this wrong on song select and just assumes that it can handle mania EZ / HR the same way as all other rulesets.
|
||||
|
||||
double perfectHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, ManiaHitWindows.PERFECT_WINDOW_RANGE);
|
||||
|
||||
if (mods.Any(m => m is ManiaModHardRock))
|
||||
perfectHitWindow /= ManiaModHardRock.HIT_WINDOW_DIFFICULTY_MULTIPLIER;
|
||||
else if (mods.Any(m => m is ManiaModEasy))
|
||||
perfectHitWindow /= ManiaModEasy.HIT_WINDOW_DIFFICULTY_MULTIPLIER;
|
||||
|
||||
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(perfectHitWindow, ManiaHitWindows.PERFECT_WINDOW_RANGE);
|
||||
adjustedDifficulty.CircleSize = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), mods);
|
||||
|
||||
return adjustedDifficulty;
|
||||
}
|
||||
|
||||
public override IEnumerable<RulesetBeatmapAttribute> GetBeatmapAttributesForDisplay(IBeatmapInfo beatmapInfo, IReadOnlyCollection<Mod> mods)
|
||||
{
|
||||
// a special touch-up of key count is required to the original difficulty, since key conversion mods are not `IApplicableToDifficulty`
|
||||
var originalDifficulty = new BeatmapDifficulty(beatmapInfo.Difficulty)
|
||||
{
|
||||
CircleSize = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), [])
|
||||
};
|
||||
var adjustedDifficulty = GetAdjustedDisplayDifficulty(beatmapInfo, mods);
|
||||
var colours = new OsuColour();
|
||||
|
||||
yield return new RulesetBeatmapAttribute(SongSelectStrings.KeyCount, @"KC", originalDifficulty.CircleSize, adjustedDifficulty.CircleSize, 18)
|
||||
{
|
||||
Description = "Affects the number of key columns on the playfield."
|
||||
};
|
||||
|
||||
var hitWindows = new ManiaHitWindows();
|
||||
hitWindows.SetDifficulty(adjustedDifficulty.OverallDifficulty);
|
||||
hitWindows.IsConvert = !beatmapInfo.Ruleset.Equals(RulesetInfo);
|
||||
hitWindows.ClassicModActive = mods.Any(m => m is ManiaModClassic);
|
||||
yield return new RulesetBeatmapAttribute(SongSelectStrings.Accuracy, @"OD", originalDifficulty.OverallDifficulty, adjustedDifficulty.OverallDifficulty, 10)
|
||||
{
|
||||
Description = "Affects timing requirements for notes.",
|
||||
AdditionalMetrics = hitWindows.GetAllAvailableWindows()
|
||||
.Reverse()
|
||||
.Select(window => new RulesetBeatmapAttribute.AdditionalMetric(
|
||||
$"{window.result.GetDescription().ToUpperInvariant()} hit window",
|
||||
LocalisableString.Interpolate($@"±{hitWindows.WindowFor(window.result):0.##} ms"),
|
||||
colours.ForHitResult(window.result)
|
||||
)).ToArray()
|
||||
};
|
||||
|
||||
yield return new RulesetBeatmapAttribute(SongSelectStrings.HPDrain, @"HP", originalDifficulty.DrainRate, adjustedDifficulty.DrainRate, 10)
|
||||
{
|
||||
Description = "Affects the harshness of health drain and the health penalties for missing."
|
||||
};
|
||||
}
|
||||
|
||||
public override IRulesetFilterCriteria CreateRulesetFilterCriteria()
|
||||
{
|
||||
return new ManiaFilterCriteria();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -48,12 +50,21 @@ namespace osu.Game.Rulesets.Mania
|
||||
},
|
||||
};
|
||||
|
||||
Add(new SettingsCheckbox
|
||||
{
|
||||
LabelText = RulesetSettingsStrings.TouchOverlay,
|
||||
Current = config.GetBindable<bool>(ManiaRulesetSetting.TouchOverlay)
|
||||
});
|
||||
|
||||
if (RuntimeInfo.IsMobile)
|
||||
{
|
||||
Add(new SettingsEnumDropdown<ManiaMobileLayout>
|
||||
{
|
||||
LabelText = 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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override string Acronym => Name;
|
||||
public abstract int KeyCount { get; }
|
||||
public override ModType Type => ModType.Conversion;
|
||||
public override double ScoreMultiplier => 1; // TODO: Implement the mania key mod score multiplier
|
||||
public override double ScoreMultiplier => 0.9;
|
||||
public override bool Ranked => UsesDefaultConfiguration;
|
||||
|
||||
public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
public override LocalisableString Description => "No more tricky speed changes!";
|
||||
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Equals;
|
||||
public override IconUsage? Icon => OsuIcon.ModConstantSpeed;
|
||||
|
||||
public override ModType Type => ModType.Conversion;
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
@@ -14,6 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public override string Name => "Cover";
|
||||
public override string Acronym => "CO";
|
||||
public override IconUsage? Icon => OsuIcon.ModCover;
|
||||
|
||||
public override LocalisableString Description => @"Decrease the playfield's viewing area.";
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@@ -13,6 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override string Name => "Dual Stages";
|
||||
public override string Acronym => "DS";
|
||||
public override LocalisableString Description => @"Double the stages, double the fun!";
|
||||
public override IconUsage? Icon => OsuIcon.ModDualStages;
|
||||
public override ModType Type => ModType.Conversion;
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
|
||||
@@ -13,19 +13,19 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and extra lives!";
|
||||
|
||||
public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1 / 1.4;
|
||||
|
||||
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
|
||||
{
|
||||
const double multiplier = 1 / 1.4;
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
case Note:
|
||||
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = multiplier;
|
||||
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
|
||||
break;
|
||||
|
||||
case HoldNote hold:
|
||||
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = multiplier;
|
||||
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = multiplier;
|
||||
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
|
||||
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
@@ -12,6 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public override string Name => "Fade In";
|
||||
public override string Acronym => "FI";
|
||||
public override IconUsage? Icon => OsuIcon.ModFadeIn;
|
||||
public override LocalisableString Description => @"Keys appear out of nowhere!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool ValidForFreestyleAsRequiredMod => false;
|
||||
|
||||
@@ -13,19 +13,19 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool Ranked => false;
|
||||
|
||||
public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1.4;
|
||||
|
||||
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
|
||||
{
|
||||
const double multiplier = 1.4;
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
case Note:
|
||||
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = multiplier;
|
||||
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
|
||||
break;
|
||||
|
||||
case HoldNote hold:
|
||||
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = multiplier;
|
||||
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = multiplier;
|
||||
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
|
||||
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
public override LocalisableString Description => @"Replaces all hold notes with normal notes.";
|
||||
|
||||
public override IconUsage? Icon => FontAwesome.Solid.DotCircle;
|
||||
public override IconUsage? Icon => OsuIcon.ModHoldOff;
|
||||
|
||||
public override ModType Type => ModType.Conversion;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
public override LocalisableString Description => "Hold the keys. To the beat.";
|
||||
|
||||
public override IconUsage? Icon => FontAwesome.Solid.YinYang;
|
||||
public override IconUsage? Icon => OsuIcon.ModInvert;
|
||||
|
||||
public override ModType Type => ModType.Conversion;
|
||||
|
||||
@@ -63,6 +64,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
StartTime = locations[i].startTime,
|
||||
Duration = duration,
|
||||
NodeSamples = new List<IList<HitSampleInfo>> { locations[i].samples, Array.Empty<HitSampleInfo>() }
|
||||
// intentionally don't play sliding samples here, it doesn't work in this mod.
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 1;
|
||||
public override string Name => "One Key";
|
||||
public override string Acronym => "1K";
|
||||
public override IconUsage? Icon => OsuIcon.ModOneKey;
|
||||
public override LocalisableString Description => @"Play with one key.";
|
||||
public override bool Ranked => false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 10;
|
||||
public override string Name => "Ten Keys";
|
||||
public override string Acronym => "10K";
|
||||
public override IconUsage? Icon => OsuIcon.ModTenKeys;
|
||||
public override LocalisableString Description => @"Play with ten keys.";
|
||||
public override bool Ranked => false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 2;
|
||||
public override string Name => "Two Keys";
|
||||
public override string Acronym => "2K";
|
||||
public override IconUsage? Icon => OsuIcon.ModTwoKeys;
|
||||
public override LocalisableString Description => @"Play with two keys.";
|
||||
public override bool Ranked => false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 3;
|
||||
public override string Name => "Three Keys";
|
||||
public override string Acronym => "3K";
|
||||
public override IconUsage? Icon => OsuIcon.ModThreeKeys;
|
||||
public override LocalisableString Description => @"Play with three keys.";
|
||||
public override bool Ranked => false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 4;
|
||||
public override string Name => "Four Keys";
|
||||
public override string Acronym => "4K";
|
||||
public override IconUsage? Icon => OsuIcon.ModFourKeys;
|
||||
public override LocalisableString Description => @"Play with four keys.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
@@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 5;
|
||||
public override string Name => "Five Keys";
|
||||
public override string Acronym => "5K";
|
||||
public override IconUsage? Icon => OsuIcon.ModFiveKeys;
|
||||
public override LocalisableString Description => @"Play with five keys.";
|
||||
}
|
||||
}
|
||||
|
||||