mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 10:12:54 +08:00
Merge branch 'master' of https://github.com/ppy/osu into smoke
This commit is contained in:
commit
0316e3e706
@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"jetbrains.resharper.globaltools": {
|
||||
"version": "2022.1.1",
|
||||
"version": "2022.2.3",
|
||||
"commands": [
|
||||
"jb"
|
||||
]
|
||||
@ -21,7 +21,7 @@
|
||||
]
|
||||
},
|
||||
"ppy.localisationanalyser.tools": {
|
||||
"version": "2022.607.0",
|
||||
"version": "2022.809.0",
|
||||
"commands": [
|
||||
"localisation"
|
||||
]
|
||||
|
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -1,5 +1,8 @@
|
||||
on: [push, pull_request]
|
||||
name: Continuous Integration
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
inspect-code:
|
||||
|
@ -53,3 +53,7 @@ dotnet_diagnostic.CA2225.severity = none
|
||||
|
||||
# Banned APIs
|
||||
dotnet_diagnostic.RS0030.severity = error
|
||||
|
||||
# Temporarily disable analysing CanBeNull = true in NRT contexts due to mobile issues.
|
||||
# See: https://github.com/ppy/osu/pull/19677
|
||||
dotnet_diagnostic.OSUF001.severity = none
|
@ -6,8 +6,6 @@ T:System.IComparable;Don't use non-generic IComparable. Use generic version inst
|
||||
T:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable<T> instead.
|
||||
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
|
||||
M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
|
||||
T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
|
||||
T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.
|
||||
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
|
||||
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
|
||||
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead.
|
||||
|
@ -12,7 +12,6 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
|
||||
|
@ -77,5 +77,8 @@ namespace osu.Game.Rulesets.EmptyFreeform
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Leave this line intact. It will bake the correct version into the ruleset on each build/release.
|
||||
public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION;
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||
|
@ -49,5 +49,8 @@ namespace osu.Game.Rulesets.Pippidon
|
||||
};
|
||||
|
||||
public override Drawable CreateIcon() => new PippidonRulesetIcon(this);
|
||||
|
||||
// Leave this line intact. It will bake the correct version into the ruleset on each build/release.
|
||||
public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION;
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
||||
|
@ -54,5 +54,8 @@ namespace osu.Game.Rulesets.EmptyScrolling
|
||||
Text = ShortName[0].ToString(),
|
||||
Font = OsuFont.Default.With(size: 18),
|
||||
};
|
||||
|
||||
// Leave this line intact. It will bake the correct version into the ruleset on each build/release.
|
||||
public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION;
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||
|
@ -21,8 +21,11 @@ namespace osu.Game.Rulesets.Pippidon.Beatmaps
|
||||
public PippidonBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
|
||||
: base(beatmap, ruleset)
|
||||
{
|
||||
minPosition = beatmap.HitObjects.Min(getUsablePosition);
|
||||
maxPosition = beatmap.HitObjects.Max(getUsablePosition);
|
||||
if (beatmap.HitObjects.Any())
|
||||
{
|
||||
minPosition = beatmap.HitObjects.Min(getUsablePosition);
|
||||
maxPosition = beatmap.HitObjects.Max(getUsablePosition);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition && h is IHasYPosition);
|
||||
|
@ -46,5 +46,8 @@ namespace osu.Game.Rulesets.Pippidon
|
||||
};
|
||||
|
||||
public override Drawable CreateIcon() => new PippidonRulesetIcon(this);
|
||||
|
||||
// Leave this line intact. It will bake the correct version into the ruleset on each build/release.
|
||||
public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION;
|
||||
}
|
||||
}
|
||||
|
@ -51,11 +51,11 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.810.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.810.2" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.831.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.916.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
<PackageReference Include="Realm" Version="10.14.0" />
|
||||
<PackageReference Include="Realm" Version="10.15.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -76,7 +76,7 @@ namespace osu.Desktop.Security
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Icon = FontAwesome.Solid.ShieldAlt;
|
||||
IconBackground.Colour = colours.YellowDark;
|
||||
IconContent.Colour = colours.YellowDark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,29 +5,24 @@ using System;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using Squirrel;
|
||||
using Squirrel.SimpleSplat;
|
||||
using LogLevel = Squirrel.SimpleSplat.LogLevel;
|
||||
using UpdateManager = osu.Game.Updater.UpdateManager;
|
||||
|
||||
namespace osu.Desktop.Updater
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager
|
||||
public class SquirrelUpdateManager : UpdateManager
|
||||
{
|
||||
private UpdateManager? updateManager;
|
||||
private Squirrel.UpdateManager? updateManager;
|
||||
private INotificationOverlay notificationOverlay = null!;
|
||||
|
||||
public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited();
|
||||
public Task PrepareUpdateAsync() => Squirrel.UpdateManager.RestartAppWhenExited();
|
||||
|
||||
private static readonly Logger logger = Logger.GetLogger("updater");
|
||||
|
||||
@ -38,6 +33,9 @@ namespace osu.Desktop.Updater
|
||||
|
||||
private readonly SquirrelLogger squirrelLogger = new SquirrelLogger();
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(INotificationOverlay notifications)
|
||||
{
|
||||
@ -66,7 +64,14 @@ namespace osu.Desktop.Updater
|
||||
if (updatePending)
|
||||
{
|
||||
// the user may have dismissed the completion notice, so show it again.
|
||||
notificationOverlay.Post(new UpdateCompleteNotification(this));
|
||||
notificationOverlay.Post(new UpdateApplicationCompleteNotification
|
||||
{
|
||||
Activated = () =>
|
||||
{
|
||||
restartToApplyUpdate();
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -78,19 +83,21 @@ namespace osu.Desktop.Updater
|
||||
|
||||
if (notification == null)
|
||||
{
|
||||
notification = new UpdateProgressNotification(this) { State = ProgressNotificationState.Active };
|
||||
notification = new UpdateProgressNotification
|
||||
{
|
||||
CompletionClickAction = restartToApplyUpdate,
|
||||
};
|
||||
|
||||
Schedule(() => notificationOverlay.Post(notification));
|
||||
}
|
||||
|
||||
notification.Progress = 0;
|
||||
notification.Text = @"Downloading update...";
|
||||
notification.StartDownload();
|
||||
|
||||
try
|
||||
{
|
||||
await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
||||
|
||||
notification.Progress = 0;
|
||||
notification.Text = @"Installing update...";
|
||||
notification.StartInstall();
|
||||
|
||||
await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
||||
|
||||
@ -110,9 +117,7 @@ namespace osu.Desktop.Updater
|
||||
else
|
||||
{
|
||||
// In the case of an error, a separate notification will be displayed.
|
||||
notification.State = ProgressNotificationState.Cancelled;
|
||||
notification.Close();
|
||||
|
||||
notification.FailDownload();
|
||||
Logger.Error(e, @"update failed!");
|
||||
}
|
||||
}
|
||||
@ -134,84 +139,24 @@ namespace osu.Desktop.Updater
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool restartToApplyUpdate()
|
||||
{
|
||||
PrepareUpdateAsync()
|
||||
.ContinueWith(_ => Schedule(() => game.AttemptExit()));
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
updateManager?.Dispose();
|
||||
}
|
||||
|
||||
private class UpdateCompleteNotification : ProgressCompletionNotification
|
||||
{
|
||||
[Resolved]
|
||||
private OsuGame game { get; set; } = null!;
|
||||
|
||||
public UpdateCompleteNotification(SquirrelUpdateManager updateManager)
|
||||
{
|
||||
Text = @"Update ready to install. Click to restart!";
|
||||
|
||||
Activated = () =>
|
||||
{
|
||||
updateManager.PrepareUpdateAsync()
|
||||
.ContinueWith(_ => updateManager.Schedule(() => game.AttemptExit()));
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateProgressNotification : ProgressNotification
|
||||
{
|
||||
private readonly SquirrelUpdateManager updateManager;
|
||||
|
||||
public UpdateProgressNotification(SquirrelUpdateManager updateManager)
|
||||
{
|
||||
this.updateManager = updateManager;
|
||||
}
|
||||
|
||||
protected override Notification CreateCompletionNotification()
|
||||
{
|
||||
return new UpdateCompleteNotification(updateManager);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
IconContent.AddRange(new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientVertical(colours.YellowDark, colours.Yellow)
|
||||
},
|
||||
new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Icon = FontAwesome.Solid.Upload,
|
||||
Colour = Color4.White,
|
||||
Size = new Vector2(20),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
// cancelling updates is not currently supported by the underlying updater.
|
||||
// only allow dismissing for now.
|
||||
|
||||
switch (State)
|
||||
{
|
||||
case ProgressNotificationState.Cancelled:
|
||||
base.Close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SquirrelLogger : ILogger, IDisposable
|
||||
{
|
||||
public Squirrel.SimpleSplat.LogLevel Level { get; set; } = Squirrel.SimpleSplat.LogLevel.Info;
|
||||
public LogLevel Level { get; set; } = LogLevel.Info;
|
||||
|
||||
public void Write(string message, Squirrel.SimpleSplat.LogLevel logLevel)
|
||||
public void Write(string message, LogLevel logLevel)
|
||||
{
|
||||
if (logLevel < Level)
|
||||
return;
|
||||
|
@ -27,11 +27,6 @@
|
||||
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
|
||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.14">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.0.175" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
|
@ -70,10 +70,17 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
[Cached]
|
||||
private readonly BindableBeatDivisor beatDivisor;
|
||||
|
||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
public EditorBeatmapDependencyContainer(IBeatmap beatmap, BindableBeatDivisor beatDivisor)
|
||||
{
|
||||
editorClock = new EditorClock(beatmap, beatDivisor);
|
||||
this.beatDivisor = beatDivisor;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
editorClock = new EditorClock(beatmap, beatDivisor),
|
||||
Content,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
43
osu.Game.Rulesets.Catch.Tests/TestSceneCatchTouchInput.cs
Normal file
43
osu.Game.Rulesets.Catch.Tests/TestSceneCatchTouchInput.cs
Normal file
@ -0,0 +1,43 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneCatchTouchInput : OsuTestScene
|
||||
{
|
||||
private CatchTouchInputMapper catchTouchInputMapper = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create input overlay", () =>
|
||||
{
|
||||
Child = new CatchInputManager(new CatchRuleset().RulesetInfo)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
catchTouchInputMapper = new CatchTouchInputMapper
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("show overlay", () => catchTouchInputMapper.Show());
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
@ -4,11 +4,13 @@
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch
|
||||
{
|
||||
[Cached]
|
||||
public class CatchInputManager : RulesetInputManager<CatchAction>
|
||||
{
|
||||
public CatchInputManager(RulesetInfo ruleset)
|
||||
|
@ -1,38 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Difficulty;
|
||||
using osu.Game.Rulesets.Catch.Scoring;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using System;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Game.Rulesets.Catch.Edit;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
using osu.Game.Rulesets.Catch.Scoring;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Legacy;
|
||||
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.Replays.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch
|
||||
{
|
||||
public class CatchRuleset : Ruleset, ILegacyRuleset
|
||||
{
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableCatchRuleset(this, beatmap, mods);
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableCatchRuleset(this, beatmap, mods);
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor();
|
||||
|
||||
@ -42,6 +41,8 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
public const string SHORT_NAME = "fruits";
|
||||
|
||||
public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION;
|
||||
|
||||
public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0) => new[]
|
||||
{
|
||||
new KeyBinding(InputKey.Z, CatchAction.MoveLeft),
|
||||
@ -162,7 +163,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
};
|
||||
}
|
||||
|
||||
public override string GetDisplayNameForHitResult(HitResult result)
|
||||
public override LocalisableString GetDisplayNameForHitResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
@ -181,7 +182,16 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new CatchDifficultyCalculator(RulesetInfo, beatmap);
|
||||
|
||||
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new CatchLegacySkinTransformer(skin);
|
||||
public override ISkin? CreateSkinTransformer(ISkin skin, IBeatmap beatmap)
|
||||
{
|
||||
switch (skin)
|
||||
{
|
||||
case LegacySkin:
|
||||
return new CatchLegacySkinTransformer(skin);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override PerformanceCalculator CreatePerformanceCalculator() => new CatchPerformanceCalculator();
|
||||
|
||||
|
@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
|
||||
{
|
||||
while (path.Vertices.Count < InternalChildren.Count)
|
||||
RemoveInternal(InternalChildren[^1]);
|
||||
RemoveInternal(InternalChildren[^1], true);
|
||||
|
||||
while (InternalChildren.Count < path.Vertices.Count)
|
||||
AddInternal(new VertexPiece());
|
||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
.Where(h => !(h is TinyDroplet)));
|
||||
|
||||
while (nestedHitObjects.Count < InternalChildren.Count)
|
||||
RemoveInternal(InternalChildren[^1]);
|
||||
RemoveInternal(InternalChildren[^1], true);
|
||||
|
||||
while (InternalChildren.Count < nestedHitObjects.Count)
|
||||
AddInternal(new FruitOutline());
|
||||
|
@ -1,12 +1,13 @@
|
||||
// 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.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModEasy : ModEasyWithExtraLives
|
||||
{
|
||||
public override string Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
|
||||
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public override string Name => "Floating Fruits";
|
||||
public override string Acronym => "FF";
|
||||
public override string Description => "The fruits are... floating?";
|
||||
public override LocalisableString Description => "The fruits are... floating?";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Cloud;
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset<CatchHitObject>
|
||||
{
|
||||
public override string Description => @"Play with fading fruits.";
|
||||
public override LocalisableString Description => @"Play with fading fruits.";
|
||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
|
||||
|
||||
private const double fade_out_offset_multiplier = 0.6;
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModMirror : ModMirror, IApplicableToBeatmap
|
||||
{
|
||||
public override string Description => "Fruits are flipped horizontally.";
|
||||
public override LocalisableString Description => "Fruits are flipped horizontally.";
|
||||
|
||||
/// <remarks>
|
||||
/// <see cref="IApplicableToBeatmap"/> is used instead of <see cref="IApplicableToHitObject"/>,
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModNoScope : ModNoScope, IUpdatableByPlayfield
|
||||
{
|
||||
public override string Description => "Where's the catcher?";
|
||||
public override LocalisableString Description => "Where's the catcher?";
|
||||
|
||||
[SettingSource(
|
||||
"Hidden at combo",
|
||||
|
@ -5,6 +5,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset<CatchHitObject>, IApplicableToPlayer
|
||||
{
|
||||
public override string Description => @"Use the mouse to control the catcher.";
|
||||
public override LocalisableString Description => @"Use the mouse to control the catcher.";
|
||||
|
||||
private DrawableRuleset<CatchHitObject> drawableRuleset = null!;
|
||||
|
||||
|
277
osu.Game.Rulesets.Catch/UI/CatchTouchInputMapper.cs
Normal file
277
osu.Game.Rulesets.Catch/UI/CatchTouchInputMapper.cs
Normal file
@ -0,0 +1,277 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
public class CatchTouchInputMapper : VisibilityContainer
|
||||
{
|
||||
public override bool PropagatePositionalInputSubTree => true;
|
||||
public override bool PropagateNonPositionalInputSubTree => true;
|
||||
|
||||
private readonly Dictionary<object, TouchCatchAction> trackedActionSources = new Dictionary<object, TouchCatchAction>();
|
||||
|
||||
private KeyBindingContainer<CatchAction> keyBindingContainer = null!;
|
||||
|
||||
private Container mainContent = null!;
|
||||
|
||||
private InputArea leftBox = null!;
|
||||
private InputArea rightBox = null!;
|
||||
private InputArea leftDashBox = null!;
|
||||
private InputArea rightDashBox = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(CatchInputManager catchInputManager, OsuColour colours)
|
||||
{
|
||||
const float width = 0.15f;
|
||||
|
||||
keyBindingContainer = catchInputManager.KeyBindingContainer;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
mainContent = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = width,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
leftDashBox = new InputArea(TouchCatchAction.DashLeft, trackedActionSources)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
},
|
||||
leftBox = new InputArea(TouchCatchAction.MoveLeft, trackedActionSources)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
Colour = colours.Gray9,
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
},
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = width,
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
rightBox = new InputArea(TouchCatchAction.MoveRight, trackedActionSources)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
Colour = colours.Gray9,
|
||||
},
|
||||
rightDashBox = new InputArea(TouchCatchAction.DashRight, trackedActionSources)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
// Hide whenever the keyboard is used.
|
||||
Hide();
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
return updateAction(e.Button, getTouchCatchActionFromInput(e.ScreenSpaceMousePosition));
|
||||
}
|
||||
|
||||
protected override bool OnTouchDown(TouchDownEvent e)
|
||||
{
|
||||
return updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
|
||||
}
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
Show();
|
||||
|
||||
TouchCatchAction? action = getTouchCatchActionFromInput(e.ScreenSpaceMousePosition);
|
||||
|
||||
// multiple mouse buttons may be pressed and handling the same action.
|
||||
foreach (MouseButton button in e.PressedButtons)
|
||||
updateAction(button, action);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnTouchMove(TouchMoveEvent e)
|
||||
{
|
||||
updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
|
||||
base.OnTouchMove(e);
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
updateAction(e.Button, null);
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
protected override void OnTouchUp(TouchUpEvent e)
|
||||
{
|
||||
updateAction(e.Touch.Source, null);
|
||||
base.OnTouchUp(e);
|
||||
}
|
||||
|
||||
private bool updateAction(object source, TouchCatchAction? newAction)
|
||||
{
|
||||
TouchCatchAction? actionBefore = null;
|
||||
|
||||
if (trackedActionSources.TryGetValue(source, out TouchCatchAction found))
|
||||
actionBefore = found;
|
||||
|
||||
if (actionBefore != newAction)
|
||||
{
|
||||
if (newAction != null)
|
||||
trackedActionSources[source] = newAction.Value;
|
||||
else
|
||||
trackedActionSources.Remove(source);
|
||||
|
||||
updatePressedActions();
|
||||
}
|
||||
|
||||
return newAction != null;
|
||||
}
|
||||
|
||||
private void updatePressedActions()
|
||||
{
|
||||
Show();
|
||||
|
||||
if (trackedActionSources.ContainsValue(TouchCatchAction.DashLeft) || trackedActionSources.ContainsValue(TouchCatchAction.MoveLeft))
|
||||
keyBindingContainer.TriggerPressed(CatchAction.MoveLeft);
|
||||
else
|
||||
keyBindingContainer.TriggerReleased(CatchAction.MoveLeft);
|
||||
|
||||
if (trackedActionSources.ContainsValue(TouchCatchAction.DashRight) || trackedActionSources.ContainsValue(TouchCatchAction.MoveRight))
|
||||
keyBindingContainer.TriggerPressed(CatchAction.MoveRight);
|
||||
else
|
||||
keyBindingContainer.TriggerReleased(CatchAction.MoveRight);
|
||||
|
||||
if (trackedActionSources.ContainsValue(TouchCatchAction.DashLeft) || trackedActionSources.ContainsValue(TouchCatchAction.DashRight))
|
||||
keyBindingContainer.TriggerPressed(CatchAction.Dash);
|
||||
else
|
||||
keyBindingContainer.TriggerReleased(CatchAction.Dash);
|
||||
}
|
||||
|
||||
private TouchCatchAction? getTouchCatchActionFromInput(Vector2 screenSpaceInputPosition)
|
||||
{
|
||||
if (leftDashBox.Contains(screenSpaceInputPosition))
|
||||
return TouchCatchAction.DashLeft;
|
||||
if (rightDashBox.Contains(screenSpaceInputPosition))
|
||||
return TouchCatchAction.DashRight;
|
||||
if (leftBox.Contains(screenSpaceInputPosition))
|
||||
return TouchCatchAction.MoveLeft;
|
||||
if (rightBox.Contains(screenSpaceInputPosition))
|
||||
return TouchCatchAction.MoveRight;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override void PopIn() => mainContent.FadeIn(300, Easing.OutQuint);
|
||||
|
||||
protected override void PopOut() => mainContent.FadeOut(300, Easing.OutQuint);
|
||||
|
||||
private class InputArea : CompositeDrawable, IKeyBindingHandler<CatchAction>
|
||||
{
|
||||
private readonly TouchCatchAction handledAction;
|
||||
|
||||
private readonly Box highlightOverlay;
|
||||
|
||||
private readonly IEnumerable<KeyValuePair<object, TouchCatchAction>> trackedActions;
|
||||
|
||||
private bool isHighlighted;
|
||||
|
||||
public InputArea(TouchCatchAction handledAction, IEnumerable<KeyValuePair<object, TouchCatchAction>> trackedActions)
|
||||
{
|
||||
this.handledAction = handledAction;
|
||||
this.trackedActions = trackedActions;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 10,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.15f,
|
||||
},
|
||||
highlightOverlay = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
Blending = BlendingParameters.Additive,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<CatchAction> _)
|
||||
{
|
||||
updateHighlight();
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<CatchAction> _)
|
||||
{
|
||||
updateHighlight();
|
||||
}
|
||||
|
||||
private void updateHighlight()
|
||||
{
|
||||
bool isHandling = trackedActions.Any(a => a.Value == handledAction);
|
||||
|
||||
if (isHandling == isHighlighted)
|
||||
return;
|
||||
|
||||
isHighlighted = isHandling;
|
||||
highlightOverlay.FadeTo(isHighlighted ? 0.1f : 0, isHighlighted ? 80 : 400, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
public enum TouchCatchAction
|
||||
{
|
||||
MoveLeft,
|
||||
MoveRight,
|
||||
DashLeft,
|
||||
DashRight,
|
||||
}
|
||||
}
|
||||
}
|
@ -271,8 +271,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
SetHyperDashState();
|
||||
}
|
||||
|
||||
caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject);
|
||||
droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject);
|
||||
caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject, false);
|
||||
droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -430,7 +430,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
var droppedObject = getDroppedObject(caughtObject);
|
||||
|
||||
caughtObjectContainer.Remove(caughtObject);
|
||||
caughtObjectContainer.Remove(caughtObject, false);
|
||||
|
||||
droppedObjectTarget.Add(droppedObject);
|
||||
|
||||
|
@ -93,15 +93,15 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
switch (entry.Animation)
|
||||
{
|
||||
case CatcherTrailAnimation.Dashing:
|
||||
dashTrails.Remove(drawable);
|
||||
dashTrails.Remove(drawable, false);
|
||||
break;
|
||||
|
||||
case CatcherTrailAnimation.HyperDashing:
|
||||
hyperDashTrails.Remove(drawable);
|
||||
hyperDashTrails.Remove(drawable, false);
|
||||
break;
|
||||
|
||||
case CatcherTrailAnimation.HyperDashAfterImage:
|
||||
hyperDashAfterImages.Remove(drawable);
|
||||
hyperDashAfterImages.Remove(drawable, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
@ -32,6 +33,12 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
KeyBindingInputManager.Add(new CatchTouchInputMapper());
|
||||
}
|
||||
|
||||
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
|
||||
|
||||
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@ -34,10 +35,14 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
AddStep("setup compose screen", () =>
|
||||
{
|
||||
var editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 4 })
|
||||
{
|
||||
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
|
||||
});
|
||||
};
|
||||
|
||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
|
||||
|
||||
var editorBeatmap = new EditorBeatmap(beatmap, new LegacyBeatmapSkin(beatmap.BeatmapInfo, null));
|
||||
|
||||
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
|
||||
|
||||
@ -50,7 +55,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
(typeof(IBeatSnapProvider), editorBeatmap),
|
||||
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)),
|
||||
},
|
||||
Child = new ComposeScreen { State = { Value = Visibility.Visible } },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
editorBeatmap,
|
||||
new ComposeScreen { State = { Value = Visibility.Visible } },
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
BeatDivisor.Value = 8;
|
||||
Clock.Seek(0);
|
||||
EditorClock.Seek(0);
|
||||
|
||||
Child = composer = new TestComposer { RelativeSizeAxes = Axes.Both };
|
||||
});
|
||||
@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
|
||||
originalTime = lastObject.HitObject.StartTime;
|
||||
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
|
||||
EditorClock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
|
||||
});
|
||||
|
||||
AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
|
||||
@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
|
||||
originalTime = lastObject.HitObject.StartTime;
|
||||
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
|
||||
EditorClock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
|
||||
});
|
||||
|
||||
AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
|
||||
@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
AddStep("seek to last object", () =>
|
||||
{
|
||||
lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
|
||||
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
|
||||
EditorClock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
|
||||
});
|
||||
|
||||
AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
|
||||
|
@ -16,11 +16,11 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
|
||||
|
||||
[TestCase(2.3449735700206298d, 242, "diffcalc-test")]
|
||||
[TestCase(2.3493769750220914d, 242, "diffcalc-test")]
|
||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||
|
||||
[TestCase(2.7879104989252959d, 242, "diffcalc-test")]
|
||||
[TestCase(2.797245912537965d, 242, "diffcalc-test")]
|
||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());
|
||||
|
||||
|
@ -4,7 +4,6 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Configuration.Tracking;
|
||||
using osu.Game.Configuration;
|
||||
@ -13,7 +11,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
{
|
||||
public class ManiaRulesetConfigManager : RulesetConfigManager<ManiaRulesetSetting>
|
||||
{
|
||||
public ManiaRulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null)
|
||||
public ManiaRulesetConfigManager(SettingsStore? settings, RulesetInfo ruleset, int? variant = null)
|
||||
: base(settings, ruleset, variant)
|
||||
{
|
||||
}
|
||||
@ -33,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
scrollTime => new SettingDescription(
|
||||
rawValue: scrollTime,
|
||||
name: "Scroll Speed",
|
||||
value: $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)} ({scrollTime}ms)"
|
||||
value: $"{scrollTime}ms (speed {(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)})"
|
||||
)
|
||||
)
|
||||
};
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
private readonly bool isForCurrentRuleset;
|
||||
private readonly double originalOverallDifficulty;
|
||||
|
||||
public override int Version => 20220701;
|
||||
public override int Version => 20220902;
|
||||
|
||||
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
|
@ -21,7 +21,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
protected override double SkillMultiplier => 1;
|
||||
protected override double StrainDecayBase => 1;
|
||||
|
||||
private readonly double[] holdEndTimes;
|
||||
private readonly double[] startTimes;
|
||||
private readonly double[] endTimes;
|
||||
private readonly double[] individualStrains;
|
||||
|
||||
private double individualStrain;
|
||||
@ -30,7 +31,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
public Strain(Mod[] mods, int totalColumns)
|
||||
: base(mods)
|
||||
{
|
||||
holdEndTimes = new double[totalColumns];
|
||||
startTimes = new double[totalColumns];
|
||||
endTimes = new double[totalColumns];
|
||||
individualStrains = new double[totalColumns];
|
||||
overallStrain = 1;
|
||||
}
|
||||
@ -38,32 +40,27 @@ 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;
|
||||
double closestEndTime = Math.Abs(endTime - maniaCurrent.LastObject.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
|
||||
bool isOverlapping = false;
|
||||
|
||||
// Fill up the holdEndTimes array
|
||||
for (int i = 0; i < holdEndTimes.Length; ++i)
|
||||
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
|
||||
|
||||
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(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1);
|
||||
isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) && Precision.DefinitelyBigger(endTime, endTimes[i], 1);
|
||||
|
||||
// We give a slight bonus to everything if something is held meanwhile
|
||||
if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1))
|
||||
if (Precision.DefinitelyBigger(endTimes[i], endTime, 1))
|
||||
holdFactor = 1.25;
|
||||
|
||||
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - holdEndTimes[i]));
|
||||
|
||||
// Decay individual strains
|
||||
individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base);
|
||||
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i]));
|
||||
}
|
||||
|
||||
holdEndTimes[column] = 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
|
||||
@ -77,12 +74,22 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
if (isOverlapping)
|
||||
holdAddition = 1 / (1 + Math.Exp(0.5 * (release_threshold - closestEndTime)));
|
||||
|
||||
// Increase individual strain in own column
|
||||
// Decay and increase individualStrains in own column
|
||||
individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base);
|
||||
individualStrains[column] += 2.0 * holdFactor;
|
||||
individualStrain = individualStrains[column];
|
||||
|
||||
overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base) + (1 + holdAddition) * 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;
|
||||
|
||||
// By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section.
|
||||
return individualStrain + overallStrain - CurrentStrain;
|
||||
}
|
||||
|
||||
|
@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
private void updateBeatmap()
|
||||
{
|
||||
Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value;
|
||||
Beatmap.SaveState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,18 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Configuration;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
@ -30,13 +23,19 @@ using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Difficulty;
|
||||
using osu.Game.Rulesets.Mania.Edit;
|
||||
using osu.Game.Rulesets.Mania.Edit.Setup;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Legacy;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
@ -47,7 +46,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
/// </summary>
|
||||
public const int MAX_STAGE_KEYS = 10;
|
||||
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableManiaRuleset(this, beatmap, mods);
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableManiaRuleset(this, beatmap, mods);
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
|
||||
|
||||
@ -59,9 +58,20 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
public const string SHORT_NAME = "mania";
|
||||
|
||||
public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION;
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this);
|
||||
|
||||
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new ManiaLegacySkinTransformer(skin, beatmap);
|
||||
public override ISkin? CreateSkinTransformer(ISkin skin, IBeatmap beatmap)
|
||||
{
|
||||
switch (skin)
|
||||
{
|
||||
case LegacySkin:
|
||||
return new ManiaLegacySkinTransformer(skin, beatmap);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
|
||||
{
|
||||
@ -282,7 +292,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame();
|
||||
|
||||
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new ManiaRulesetConfigManager(settings, RulesetInfo);
|
||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new ManiaRulesetConfigManager(settings, RulesetInfo);
|
||||
|
||||
public override RulesetSettingsSubsection CreateSettings() => new ManiaSettingsSubsection(this);
|
||||
|
||||
@ -311,7 +321,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
return Array.Empty<KeyBinding>();
|
||||
}
|
||||
|
||||
public override string GetVariantName(int variant)
|
||||
public override LocalisableString GetVariantName(int variant)
|
||||
{
|
||||
switch (getPlayfieldType(variant))
|
||||
{
|
||||
@ -356,7 +366,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
};
|
||||
}
|
||||
|
||||
public override string GetDisplayNameForHitResult(HitResult result)
|
||||
public override LocalisableString GetDisplayNameForHitResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
|
@ -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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
@ -34,7 +33,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
LabelText = "Scrolling direction",
|
||||
Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
|
||||
},
|
||||
new SettingsSlider<double, TimeSlider>
|
||||
new SettingsSlider<double, ManiaScrollSlider>
|
||||
{
|
||||
LabelText = "Scroll speed",
|
||||
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
|
||||
@ -47,5 +46,10 @@ namespace osu.Game.Rulesets.Mania
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private class ManiaScrollSlider : OsuSliderBar<double>
|
||||
{
|
||||
public override LocalisableString TooltipText => $"{Current.Value}ms (speed {(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / Current.Value)})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
@ -16,9 +17,9 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
public override string Acronym => "CS";
|
||||
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override double ScoreMultiplier => 0.9;
|
||||
|
||||
public override string Description => "No more tricky speed changes!";
|
||||
public override LocalisableString Description => "No more tricky speed changes!";
|
||||
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Equals;
|
||||
|
||||
|
@ -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 osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public override string Name => "Dual Stages";
|
||||
public override string Acronym => "DS";
|
||||
public override string Description => @"Double the stages, double the fun!";
|
||||
public override LocalisableString Description => @"Double the stages, double the fun!";
|
||||
public override ModType Type => ModType.Conversion;
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
// 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.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModEasy : ModEasyWithExtraLives
|
||||
{
|
||||
public override string Description => @"More forgiving HP drain, less accuracy required, and three lives!";
|
||||
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and three lives!";
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public override string Name => "Fade In";
|
||||
public override string Acronym => "FI";
|
||||
public override string Description => @"Keys appear out of nowhere!";
|
||||
public override LocalisableString Description => @"Keys appear out of nowhere!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray();
|
||||
|
@ -3,13 +3,14 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModHidden : ManiaModPlayfieldCover
|
||||
{
|
||||
public override string Description => @"Keys fade out before you hit them!";
|
||||
public override LocalisableString Description => @"Keys fade out before you hit them!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray();
|
||||
|
@ -8,6 +8,7 @@ using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override string Description => @"Replaces all hold notes with normal notes.";
|
||||
public override LocalisableString Description => @"Replaces all hold notes with normal notes.";
|
||||
|
||||
public override IconUsage? Icon => FontAwesome.Solid.DotCircle;
|
||||
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override string Acronym => "IN";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override string Description => "Hold the keys. To the beat.";
|
||||
public override LocalisableString Description => "Hold the keys. To the beat.";
|
||||
|
||||
public override IconUsage? Icon => FontAwesome.Solid.YinYang;
|
||||
|
||||
|
@ -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 osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey1 : ManiaKeyMod
|
||||
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 1;
|
||||
public override string Name => "One Key";
|
||||
public override string Acronym => "1K";
|
||||
public override string Description => @"Play with one key.";
|
||||
public override LocalisableString Description => @"Play with one key.";
|
||||
}
|
||||
}
|
||||
|
@ -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 osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey10 : ManiaKeyMod
|
||||
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 10;
|
||||
public override string Name => "Ten Keys";
|
||||
public override string Acronym => "10K";
|
||||
public override string Description => @"Play with ten keys.";
|
||||
public override LocalisableString Description => @"Play with ten keys.";
|
||||
}
|
||||
}
|
||||
|
@ -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 osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey2 : ManiaKeyMod
|
||||
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 2;
|
||||
public override string Name => "Two Keys";
|
||||
public override string Acronym => "2K";
|
||||
public override string Description => @"Play with two keys.";
|
||||
public override LocalisableString Description => @"Play with two keys.";
|
||||
}
|
||||
}
|
||||
|
@ -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 osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey3 : ManiaKeyMod
|
||||
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 3;
|
||||
public override string Name => "Three Keys";
|
||||
public override string Acronym => "3K";
|
||||
public override string Description => @"Play with three keys.";
|
||||
public override LocalisableString Description => @"Play with three keys.";
|
||||
}
|
||||
}
|
||||
|
@ -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 osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey4 : ManiaKeyMod
|
||||
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 4;
|
||||
public override string Name => "Four Keys";
|
||||
public override string Acronym => "4K";
|
||||
public override string Description => @"Play with four keys.";
|
||||
public override LocalisableString Description => @"Play with four keys.";
|
||||
}
|
||||
}
|
||||
|
@ -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 osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey5 : ManiaKeyMod
|
||||
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 5;
|
||||
public override string Name => "Five Keys";
|
||||
public override string Acronym => "5K";
|
||||
public override string Description => @"Play with five keys.";
|
||||
public override LocalisableString Description => @"Play with five keys.";
|
||||
}
|
||||
}
|
||||
|
@ -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 osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey6 : ManiaKeyMod
|
||||
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 6;
|
||||
public override string Name => "Six Keys";
|
||||
public override string Acronym => "6K";
|
||||
public override string Description => @"Play with six keys.";
|
||||
public override LocalisableString Description => @"Play with six keys.";
|
||||
}
|
||||
}
|
||||
|
@ -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 osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey7 : ManiaKeyMod
|
||||
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 7;
|
||||
public override string Name => "Seven Keys";
|
||||
public override string Acronym => "7K";
|
||||
public override string Description => @"Play with seven keys.";
|
||||
public override LocalisableString Description => @"Play with seven keys.";
|
||||
}
|
||||
}
|
||||
|
@ -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 osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey8 : ManiaKeyMod
|
||||
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 8;
|
||||
public override string Name => "Eight Keys";
|
||||
public override string Acronym => "8K";
|
||||
public override string Description => @"Play with eight keys.";
|
||||
public override LocalisableString Description => @"Play with eight keys.";
|
||||
}
|
||||
}
|
||||
|
@ -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 osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey9 : ManiaKeyMod
|
||||
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 9;
|
||||
public override string Name => "Nine Keys";
|
||||
public override string Acronym => "9K";
|
||||
public override string Description => @"Play with nine keys.";
|
||||
public override LocalisableString Description => @"Play with nine keys.";
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using System.Linq;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
|
||||
@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModMirror : ModMirror, IApplicableToBeatmap
|
||||
{
|
||||
public override string Description => "Notes are flipped horizontally.";
|
||||
public override LocalisableString Description => "Notes are flipped horizontally.";
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer;
|
||||
Container hocParent = (Container)hoc.Parent;
|
||||
|
||||
hocParent.Remove(hoc);
|
||||
hocParent.Remove(hoc, false);
|
||||
hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c =>
|
||||
{
|
||||
c.RelativeSizeAxes = Axes.Both;
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModRandom : ModRandom, IApplicableToBeatmap
|
||||
{
|
||||
public override string Description => @"Shuffle around the keys!";
|
||||
public override LocalisableString Description => @"Shuffle around the keys!";
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
|
@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
else
|
||||
{
|
||||
lightContainer.FadeOut(120)
|
||||
.OnComplete(d => Column.TopLevelContainer.Remove(d));
|
||||
.OnComplete(d => Column.TopLevelContainer.Remove(d, false));
|
||||
}
|
||||
}
|
||||
|
||||
|
299
osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs
Normal file
299
osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs
Normal file
@ -0,0 +1,299 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public class TestSceneObjectMerging : TestSceneOsuEditor
|
||||
{
|
||||
private OsuSelectionHandler selectionHandler => Editor.ChildrenOfType<OsuSelectionHandler>().First();
|
||||
|
||||
[Test]
|
||||
public void TestSimpleMerge()
|
||||
{
|
||||
HitCircle? circle1 = null;
|
||||
HitCircle? circle2 = null;
|
||||
|
||||
AddStep("select first two circles", () =>
|
||||
{
|
||||
circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle);
|
||||
circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h != circle1);
|
||||
EditorClock.Seek(circle1.StartTime);
|
||||
EditorBeatmap.SelectedHitObjects.Add(circle1);
|
||||
EditorBeatmap.SelectedHitObjects.Add(circle2);
|
||||
});
|
||||
|
||||
moveMouseToHitObject(1);
|
||||
AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection"));
|
||||
|
||||
mergeSelection();
|
||||
|
||||
AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor(
|
||||
(pos: circle1.Position, pathType: PathType.Linear),
|
||||
(pos: circle2.Position, pathType: null)));
|
||||
|
||||
AddStep("undo", () => Editor.Undo());
|
||||
AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && objectsRestored(circle1, circle2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMergeCircleSlider()
|
||||
{
|
||||
HitCircle? circle1 = null;
|
||||
Slider? slider = null;
|
||||
HitCircle? circle2 = null;
|
||||
|
||||
AddStep("select a circle, slider, circle", () =>
|
||||
{
|
||||
circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle);
|
||||
slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > circle1.StartTime);
|
||||
circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h.StartTime > slider.StartTime);
|
||||
EditorClock.Seek(circle1.StartTime);
|
||||
EditorBeatmap.SelectedHitObjects.Add(circle1);
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
EditorBeatmap.SelectedHitObjects.Add(circle2);
|
||||
});
|
||||
|
||||
mergeSelection();
|
||||
|
||||
AddAssert("slider created", () =>
|
||||
{
|
||||
if (circle1 is null || circle2 is null || slider is null)
|
||||
return false;
|
||||
|
||||
var controlPoints = slider.Path.ControlPoints;
|
||||
(Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints.Count + 2];
|
||||
args[0] = (circle1.Position, PathType.Linear);
|
||||
|
||||
for (int i = 0; i < controlPoints.Count; i++)
|
||||
{
|
||||
args[i + 1] = (controlPoints[i].Position + slider.Position, i == controlPoints.Count - 1 ? PathType.Linear : controlPoints[i].Type);
|
||||
}
|
||||
|
||||
args[^1] = (circle2.Position, null);
|
||||
return sliderCreatedFor(args);
|
||||
});
|
||||
|
||||
AddAssert("samples exist", sliderSampleExist);
|
||||
|
||||
AddStep("undo", () => Editor.Undo());
|
||||
AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && slider is not null && objectsRestored(circle1, slider, circle2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMergeSliderSlider()
|
||||
{
|
||||
Slider? slider1 = null;
|
||||
SliderPath? slider1Path = null;
|
||||
Slider? slider2 = null;
|
||||
|
||||
AddStep("select two sliders", () =>
|
||||
{
|
||||
slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
|
||||
slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value);
|
||||
slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime);
|
||||
EditorClock.Seek(slider1.StartTime);
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider1);
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider2);
|
||||
});
|
||||
|
||||
mergeSelection();
|
||||
|
||||
AddAssert("slider created", () =>
|
||||
{
|
||||
if (slider1 is null || slider2 is null || slider1Path is null)
|
||||
return false;
|
||||
|
||||
var controlPoints1 = slider1Path.ControlPoints;
|
||||
var controlPoints2 = slider2.Path.ControlPoints;
|
||||
(Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1];
|
||||
|
||||
for (int i = 0; i < controlPoints1.Count - 1; i++)
|
||||
{
|
||||
args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type);
|
||||
}
|
||||
|
||||
for (int i = 0; i < controlPoints2.Count; i++)
|
||||
{
|
||||
args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type);
|
||||
}
|
||||
|
||||
return sliderCreatedFor(args);
|
||||
});
|
||||
|
||||
AddAssert("samples exist", sliderSampleExist);
|
||||
|
||||
AddAssert("merged slider matches first slider", () =>
|
||||
{
|
||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||
return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
|
||||
&& mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
|
||||
&& mergedSlider.Samples.SequenceEqual(slider1.Samples)
|
||||
&& mergedSlider.SampleControlPoint.IsRedundant(slider1.SampleControlPoint);
|
||||
});
|
||||
|
||||
AddAssert("slider end is at same completion for last slider", () =>
|
||||
{
|
||||
if (slider1Path is null || slider2 is null)
|
||||
return false;
|
||||
|
||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||
return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNonMerge()
|
||||
{
|
||||
HitCircle? circle1 = null;
|
||||
HitCircle? circle2 = null;
|
||||
Spinner? spinner = null;
|
||||
|
||||
AddStep("select first two circles and spinner", () =>
|
||||
{
|
||||
circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle);
|
||||
circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h != circle1);
|
||||
spinner = (Spinner)EditorBeatmap.HitObjects.First(h => h is Spinner);
|
||||
EditorClock.Seek(spinner.StartTime);
|
||||
EditorBeatmap.SelectedHitObjects.Add(circle1);
|
||||
EditorBeatmap.SelectedHitObjects.Add(circle2);
|
||||
EditorBeatmap.SelectedHitObjects.Add(spinner);
|
||||
});
|
||||
|
||||
mergeSelection();
|
||||
|
||||
AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor(
|
||||
(pos: circle1.Position, pathType: PathType.Linear),
|
||||
(pos: circle2.Position, pathType: null)));
|
||||
|
||||
AddAssert("samples exist", sliderSampleExist);
|
||||
|
||||
AddAssert("spinner not merged", () => EditorBeatmap.HitObjects.Contains(spinner));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestIllegalMerge()
|
||||
{
|
||||
HitCircle? circle1 = null;
|
||||
HitCircle? circle2 = null;
|
||||
|
||||
AddStep("add two circles on the same position", () =>
|
||||
{
|
||||
circle1 = new HitCircle();
|
||||
circle2 = new HitCircle { Position = circle1.Position + Vector2.UnitX };
|
||||
EditorClock.Seek(0);
|
||||
EditorBeatmap.Add(circle1);
|
||||
EditorBeatmap.Add(circle2);
|
||||
EditorBeatmap.SelectedHitObjects.Add(circle1);
|
||||
EditorBeatmap.SelectedHitObjects.Add(circle2);
|
||||
});
|
||||
|
||||
moveMouseToHitObject(1);
|
||||
AddAssert("merge option not available", () => selectionHandler.ContextMenuItems.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection"));
|
||||
mergeSelection();
|
||||
AddAssert("circles not merged", () => circle1 is not null && circle2 is not null
|
||||
&& EditorBeatmap.HitObjects.Contains(circle1) && EditorBeatmap.HitObjects.Contains(circle2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSameStartTimeMerge()
|
||||
{
|
||||
HitCircle? circle1 = null;
|
||||
HitCircle? circle2 = null;
|
||||
|
||||
AddStep("add two circles at the same time", () =>
|
||||
{
|
||||
circle1 = new HitCircle();
|
||||
circle2 = new HitCircle { Position = circle1.Position + 100 * Vector2.UnitX };
|
||||
EditorClock.Seek(0);
|
||||
EditorBeatmap.Add(circle1);
|
||||
EditorBeatmap.Add(circle2);
|
||||
EditorBeatmap.SelectedHitObjects.Add(circle1);
|
||||
EditorBeatmap.SelectedHitObjects.Add(circle2);
|
||||
});
|
||||
|
||||
moveMouseToHitObject(1);
|
||||
AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection"));
|
||||
|
||||
mergeSelection();
|
||||
|
||||
AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor(
|
||||
(pos: circle1.Position, pathType: PathType.Linear),
|
||||
(pos: circle2.Position, pathType: null)));
|
||||
}
|
||||
|
||||
private void mergeSelection()
|
||||
{
|
||||
AddStep("merge selection", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LControl);
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.Key(Key.M);
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
InputManager.ReleaseKey(Key.LControl);
|
||||
});
|
||||
}
|
||||
|
||||
private bool sliderCreatedFor(params (Vector2 pos, PathType? pathType)[] expectedControlPoints)
|
||||
{
|
||||
if (EditorBeatmap.SelectedHitObjects.Count != 1)
|
||||
return false;
|
||||
|
||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||
int i = 0;
|
||||
|
||||
foreach ((Vector2 pos, PathType? pathType) in expectedControlPoints)
|
||||
{
|
||||
var controlPoint = mergedSlider.Path.ControlPoints[i++];
|
||||
|
||||
if (!Precision.AlmostEquals(controlPoint.Position + mergedSlider.Position, pos) || controlPoint.Type != pathType)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool objectsRestored(params HitObject[] objects)
|
||||
{
|
||||
foreach (var hitObject in objects)
|
||||
{
|
||||
if (EditorBeatmap.HitObjects.Contains(hitObject))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool sliderSampleExist()
|
||||
{
|
||||
if (EditorBeatmap.SelectedHitObjects.Count != 1)
|
||||
return false;
|
||||
|
||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||
|
||||
return mergedSlider.Samples[0] is not null;
|
||||
}
|
||||
|
||||
private void moveMouseToHitObject(int index)
|
||||
{
|
||||
AddStep($"hover mouse over hit object {index}", () =>
|
||||
{
|
||||
if (EditorBeatmap.HitObjects.Count <= index)
|
||||
return;
|
||||
|
||||
Vector2 position = ((OsuHitObject)EditorBeatmap.HitObjects[index]).Position;
|
||||
InputManager.MoveMouseTo(selectionHandler.ToScreenSpace(position));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -59,10 +59,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
}
|
||||
});
|
||||
|
||||
editorClock = new EditorClock(editorBeatmap);
|
||||
|
||||
base.Content.Children = new Drawable[]
|
||||
{
|
||||
editorClock = new EditorClock(editorBeatmap),
|
||||
snapProvider,
|
||||
Content
|
||||
};
|
||||
|
@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
AddStep($"click context menu item \"{contextMenuText}\"", () =>
|
||||
{
|
||||
MenuItem item = visualiser.ContextMenuItems[1].Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
|
||||
MenuItem item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == "Curve type")?.Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
|
||||
|
||||
item?.Action?.Value();
|
||||
});
|
||||
|
@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@ -55,9 +56,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(Vector2.Zero),
|
||||
new PathControlPoint(OsuPlayfield.BASE_SIZE * 2 / 5),
|
||||
new PathControlPoint(OsuPlayfield.BASE_SIZE * 3 / 5)
|
||||
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
|
||||
new PathControlPoint(new Vector2(136, 205)),
|
||||
new PathControlPoint(new Vector2(-4, 226))
|
||||
}
|
||||
}
|
||||
}));
|
||||
@ -99,8 +100,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddStep("move mouse to new point location", () =>
|
||||
{
|
||||
var firstPiece = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[0]);
|
||||
var secondPiece = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[1]);
|
||||
InputManager.MoveMouseTo((firstPiece.ScreenSpaceDrawQuad.Centre + secondPiece.ScreenSpaceDrawQuad.Centre) / 2);
|
||||
var pos = slider.Path.PositionAt(0.25d) + slider.Position;
|
||||
InputManager.MoveMouseTo(firstPiece.Parent.ToScreenSpace(pos));
|
||||
});
|
||||
AddStep("move slider end", () =>
|
||||
{
|
||||
@ -175,6 +176,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertSliderSnapped(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRotatingSliderRetainsPerfectControlPointType()
|
||||
{
|
||||
OsuSelectionHandler selectionHandler;
|
||||
|
||||
AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
|
||||
|
||||
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||
AddStep("rotate 90 degrees ccw", () =>
|
||||
{
|
||||
selectionHandler = this.ChildrenOfType<OsuSelectionHandler>().Single();
|
||||
selectionHandler.HandleRotation(-90);
|
||||
});
|
||||
|
||||
AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFlippingSliderDoesNotSnap()
|
||||
{
|
||||
@ -200,6 +218,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertSliderSnapped(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFlippingSliderRetainsPerfectControlPointType()
|
||||
{
|
||||
OsuSelectionHandler selectionHandler;
|
||||
|
||||
AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
|
||||
|
||||
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||
AddStep("flip slider horizontally", () =>
|
||||
{
|
||||
selectionHandler = this.ChildrenOfType<OsuSelectionHandler>().Single();
|
||||
selectionHandler.OnPressed(new KeyBindingPressEvent<GlobalAction>(InputManager.CurrentState, GlobalAction.EditorFlipVertically));
|
||||
});
|
||||
|
||||
AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReversingSliderDoesNotSnap()
|
||||
{
|
||||
|
255
osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs
Normal file
255
osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs
Normal file
@ -0,0 +1,255 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public class TestSceneSliderSplitting : EditorTestScene
|
||||
{
|
||||
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||
|
||||
private ComposeBlueprintContainer blueprintContainer
|
||||
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
|
||||
|
||||
private Slider? slider;
|
||||
private PathControlPointVisualiser? visualiser;
|
||||
|
||||
private const double split_gap = 100;
|
||||
|
||||
[Test]
|
||||
public void TestBasicSplit()
|
||||
{
|
||||
double endTime = 0;
|
||||
|
||||
AddStep("add slider", () =>
|
||||
{
|
||||
slider = new Slider
|
||||
{
|
||||
Position = new Vector2(0, 50),
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
|
||||
new PathControlPoint(new Vector2(150, 150)),
|
||||
new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
|
||||
new PathControlPoint(new Vector2(400, 0)),
|
||||
new PathControlPoint(new Vector2(400, 150))
|
||||
})
|
||||
};
|
||||
|
||||
EditorBeatmap.Add(slider);
|
||||
|
||||
endTime = slider.EndTime;
|
||||
});
|
||||
|
||||
AddStep("select added slider", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First();
|
||||
});
|
||||
|
||||
moveMouseToControlPoint(2);
|
||||
AddStep("select control point", () =>
|
||||
{
|
||||
if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true;
|
||||
});
|
||||
addContextMenuItemStep("Split control point");
|
||||
|
||||
AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 2 &&
|
||||
sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap,
|
||||
(new Vector2(0, 50), PathType.PerfectCurve),
|
||||
(new Vector2(150, 200), null),
|
||||
(new Vector2(300, 50), null)
|
||||
) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], slider.StartTime, endTime + split_gap,
|
||||
(new Vector2(300, 50), PathType.PerfectCurve),
|
||||
(new Vector2(400, 50), null),
|
||||
(new Vector2(400, 200), null)
|
||||
));
|
||||
|
||||
AddStep("undo", () => Editor.Undo());
|
||||
AddAssert("original slider restored", () => EditorBeatmap.HitObjects.Count == 1 && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, endTime,
|
||||
(new Vector2(0, 50), PathType.PerfectCurve),
|
||||
(new Vector2(150, 200), null),
|
||||
(new Vector2(300, 50), PathType.PerfectCurve),
|
||||
(new Vector2(400, 50), null),
|
||||
(new Vector2(400, 200), null)
|
||||
));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDoubleSplit()
|
||||
{
|
||||
double endTime = 0;
|
||||
|
||||
AddStep("add slider", () =>
|
||||
{
|
||||
slider = new Slider
|
||||
{
|
||||
Position = new Vector2(0, 50),
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
|
||||
new PathControlPoint(new Vector2(150, 150)),
|
||||
new PathControlPoint(new Vector2(300, 0), PathType.Bezier),
|
||||
new PathControlPoint(new Vector2(400, 0)),
|
||||
new PathControlPoint(new Vector2(400, 150), PathType.Catmull),
|
||||
new PathControlPoint(new Vector2(300, 200)),
|
||||
new PathControlPoint(new Vector2(400, 250))
|
||||
})
|
||||
};
|
||||
|
||||
EditorBeatmap.Add(slider);
|
||||
|
||||
endTime = slider.EndTime;
|
||||
});
|
||||
|
||||
AddStep("select added slider", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First();
|
||||
});
|
||||
|
||||
moveMouseToControlPoint(2);
|
||||
AddStep("select first control point", () =>
|
||||
{
|
||||
if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true;
|
||||
});
|
||||
moveMouseToControlPoint(4);
|
||||
AddStep("select second control point", () =>
|
||||
{
|
||||
if (visualiser is not null) visualiser.Pieces[4].IsSelected.Value = true;
|
||||
});
|
||||
addContextMenuItemStep("Split 2 control points");
|
||||
|
||||
AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 3 &&
|
||||
sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap,
|
||||
(new Vector2(0, 50), PathType.PerfectCurve),
|
||||
(new Vector2(150, 200), null),
|
||||
(new Vector2(300, 50), null)
|
||||
) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], EditorBeatmap.HitObjects[0].GetEndTime() + split_gap, slider.StartTime - split_gap,
|
||||
(new Vector2(300, 50), PathType.Bezier),
|
||||
(new Vector2(400, 50), null),
|
||||
(new Vector2(400, 200), null)
|
||||
) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[2], EditorBeatmap.HitObjects[1].GetEndTime() + split_gap, endTime + split_gap * 2,
|
||||
(new Vector2(400, 200), PathType.Catmull),
|
||||
(new Vector2(300, 250), null),
|
||||
(new Vector2(400, 300), null)
|
||||
));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSplitRetainsHitsounds()
|
||||
{
|
||||
HitSampleInfo? sample = null;
|
||||
|
||||
AddStep("add slider", () =>
|
||||
{
|
||||
slider = new Slider
|
||||
{
|
||||
Position = new Vector2(0, 50),
|
||||
LegacyLastTickOffset = 36, // This is necessary for undo to retain the sample control point
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
|
||||
new PathControlPoint(new Vector2(150, 150)),
|
||||
new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
|
||||
new PathControlPoint(new Vector2(400, 0)),
|
||||
new PathControlPoint(new Vector2(400, 150))
|
||||
})
|
||||
};
|
||||
|
||||
EditorBeatmap.Add(slider);
|
||||
});
|
||||
|
||||
AddStep("add hitsounds", () =>
|
||||
{
|
||||
if (slider is null) return;
|
||||
|
||||
slider.SampleControlPoint.SampleBank = "soft";
|
||||
slider.SampleControlPoint.SampleVolume = 70;
|
||||
sample = new HitSampleInfo("hitwhistle");
|
||||
slider.Samples.Add(sample);
|
||||
});
|
||||
|
||||
AddStep("select added slider", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First();
|
||||
});
|
||||
|
||||
moveMouseToControlPoint(2);
|
||||
AddStep("select control point", () =>
|
||||
{
|
||||
if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true;
|
||||
});
|
||||
addContextMenuItemStep("Split control point");
|
||||
AddAssert("sliders have hitsounds", hasHitsounds);
|
||||
|
||||
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0]));
|
||||
AddStep("remove first slider", () => EditorBeatmap.RemoveAt(0));
|
||||
AddStep("undo", () => Editor.Undo());
|
||||
AddAssert("sliders have hitsounds", hasHitsounds);
|
||||
|
||||
bool hasHitsounds() => sample is not null &&
|
||||
EditorBeatmap.HitObjects.All(o => o.SampleControlPoint.SampleBank == "soft" &&
|
||||
o.SampleControlPoint.SampleVolume == 70 &&
|
||||
o.Samples.Contains(sample));
|
||||
}
|
||||
|
||||
private bool sliderCreatedFor(Slider s, double startTime, double endTime, params (Vector2 pos, PathType? pathType)[] expectedControlPoints)
|
||||
{
|
||||
if (!Precision.AlmostEquals(s.StartTime, startTime, 1) || !Precision.AlmostEquals(s.EndTime, endTime, 1)) return false;
|
||||
|
||||
int i = 0;
|
||||
|
||||
foreach ((Vector2 pos, PathType? pathType) in expectedControlPoints)
|
||||
{
|
||||
var controlPoint = s.Path.ControlPoints[i++];
|
||||
|
||||
if (!Precision.AlmostEquals(controlPoint.Position + s.Position, pos) || controlPoint.Type != pathType)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void moveMouseToControlPoint(int index)
|
||||
{
|
||||
AddStep($"move mouse to control point {index}", () =>
|
||||
{
|
||||
if (slider is null || visualiser is null) return;
|
||||
|
||||
Vector2 position = slider.Path.ControlPoints[index].Position + slider.Position;
|
||||
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position));
|
||||
});
|
||||
}
|
||||
|
||||
private void addContextMenuItemStep(string contextMenuText)
|
||||
{
|
||||
AddStep($"click context menu item \"{contextMenuText}\"", () =>
|
||||
{
|
||||
if (visualiser is null) return;
|
||||
|
||||
MenuItem? item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
|
||||
|
||||
item?.Action?.Value();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
@ -125,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
/// Ensures alternation is reset before the first hitobject after a break.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestInputSingularWithBreak() => CreateModTest(new ModTestData
|
||||
public void TestInputSingularWithBreak([Values] bool pressBeforeSecondObject) => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModAlternate(),
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2,
|
||||
@ -155,21 +156,26 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
},
|
||||
}
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
ReplayFrames = new ReplayFrame[]
|
||||
{
|
||||
// first press to start alternate lock.
|
||||
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(501, new Vector2(100)),
|
||||
// press same key after break but before hit object.
|
||||
new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2251, new Vector2(300, 100)),
|
||||
new OsuReplayFrame(450, new Vector2(100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(451, new Vector2(100)),
|
||||
// press same key at second hitobject and ensure it has been hit.
|
||||
new OsuReplayFrame(2500, new Vector2(500, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2501, new Vector2(500, 100)),
|
||||
new OsuReplayFrame(2450, new Vector2(500, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2451, new Vector2(500, 100)),
|
||||
// press same key at third hitobject and ensure it has been missed.
|
||||
new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(3001, new Vector2(500, 100)),
|
||||
}
|
||||
new OsuReplayFrame(2950, new Vector2(500, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2951, new Vector2(500, 100)),
|
||||
}.Concat(!pressBeforeSecondObject
|
||||
? Enumerable.Empty<ReplayFrame>()
|
||||
: new ReplayFrame[]
|
||||
{
|
||||
// press same key after break but before hit object.
|
||||
new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2251, new Vector2(300, 100)),
|
||||
}
|
||||
).ToList()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
Mod = mod,
|
||||
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 &&
|
||||
Precision.AlmostEquals(Player.GameplayClockContainer.GameplayClock.Rate, mod.SpeedChange.Value)
|
||||
Precision.AlmostEquals(Player.GameplayClockContainer.Rate, mod.SpeedChange.Value)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -17,18 +17,18 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
|
||||
|
||||
[TestCase(6.6369583000323935d, 206, "diffcalc-test")]
|
||||
[TestCase(1.4476531024675374d, 45, "zero-length-sliders")]
|
||||
[TestCase(6.7115569159190587d, 206, "diffcalc-test")]
|
||||
[TestCase(1.4391311903612753d, 45, "zero-length-sliders")]
|
||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||
|
||||
[TestCase(8.8816128335486386d, 206, "diffcalc-test")]
|
||||
[TestCase(1.7540389962596916d, 45, "zero-length-sliders")]
|
||||
[TestCase(8.9757300665532966d, 206, "diffcalc-test")]
|
||||
[TestCase(1.7437232654020756d, 45, "zero-length-sliders")]
|
||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
||||
|
||||
[TestCase(6.6369583000323935d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4476531024675374d, 54, "zero-length-sliders")]
|
||||
[TestCase(6.7115569159190587d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4391311903612753d, 54, "zero-length-sliders")]
|
||||
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
||||
|
||||
|
@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
var drawableObject = getFunc.Invoke();
|
||||
|
||||
hitObjectContainer.Remove(drawableObject);
|
||||
hitObjectContainer.Remove(drawableObject, false);
|
||||
followPointRenderer.RemoveFollowPoints(drawableObject.HitObject);
|
||||
});
|
||||
}
|
||||
@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
else
|
||||
targetTime = getObject(hitObjectContainer.Count - 1).HitObject.StartTime + 1;
|
||||
|
||||
hitObjectContainer.Remove(toReorder);
|
||||
hitObjectContainer.Remove(toReorder, false);
|
||||
toReorder.HitObject.StartTime = targetTime;
|
||||
hitObjectContainer.Add(toReorder);
|
||||
});
|
||||
|
@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
|
||||
tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1";
|
||||
|
||||
var provider = Ruleset.Value.CreateInstance().CreateLegacySkinProvider(tintingSkin, Beatmap.Value.Beatmap);
|
||||
var provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(tintingSkin, Beatmap.Value.Beatmap);
|
||||
|
||||
Child = new SkinProvidingContainer(provider)
|
||||
{
|
||||
|
@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -12,7 +10,6 @@ using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -36,16 +33,16 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
private const double spinner_duration = 6000;
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audioManager { get; set; }
|
||||
private AudioManager audioManager { get; set; } = null!;
|
||||
|
||||
protected override bool Autoplay => true;
|
||||
|
||||
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
|
||||
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||
|
||||
private DrawableSpinner drawableSpinner;
|
||||
private DrawableSpinner drawableSpinner = null!;
|
||||
private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType<SpriteIcon>().Single();
|
||||
|
||||
[SetUpSteps]
|
||||
@ -67,12 +64,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f);
|
||||
});
|
||||
AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100));
|
||||
AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, 0, 100));
|
||||
AddAssert("is disc rotation not almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.Not.EqualTo(0).Within(100));
|
||||
AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.Not.EqualTo(0).Within(100));
|
||||
|
||||
addSeekStep(0);
|
||||
AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance));
|
||||
AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, 0, 100));
|
||||
AddAssert("is disc rotation almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(0).Within(trackerRotationTolerance));
|
||||
AddAssert("is disc rotation absolute almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(0).Within(100));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -100,20 +97,20 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
// we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
|
||||
// due to the exponential damping applied we're allowing a larger margin of error of about 10%
|
||||
// (5% relative to the final rotation value, but we're half-way through the spin).
|
||||
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation / 2, trackerRotationTolerance));
|
||||
() => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation / 2).Within(trackerRotationTolerance));
|
||||
AddAssert("symbol rotation rewound",
|
||||
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance));
|
||||
() => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation / 2).Within(spinnerSymbolRotationTolerance));
|
||||
AddAssert("is cumulative rotation rewound",
|
||||
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
|
||||
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
|
||||
() => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100));
|
||||
|
||||
addSeekStep(spinner_start_time + 5000);
|
||||
AddAssert("is disc rotation almost same",
|
||||
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
|
||||
() => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation).Within(trackerRotationTolerance));
|
||||
AddAssert("is symbol rotation almost same",
|
||||
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance));
|
||||
() => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation).Within(spinnerSymbolRotationTolerance));
|
||||
AddAssert("is cumulative rotation almost same",
|
||||
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation, 100));
|
||||
() => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -177,10 +174,10 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value);
|
||||
|
||||
addSeekStep(2000);
|
||||
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
|
||||
AddAssert("spm still valid", () => drawableSpinner.SpinsPerMinute.Value, () => Is.EqualTo(estimatedSpm).Within(1.0));
|
||||
|
||||
addSeekStep(1000);
|
||||
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
|
||||
AddAssert("spm still valid", () => drawableSpinner.SpinsPerMinute.Value, () => Is.EqualTo(estimatedSpm).Within(1.0));
|
||||
}
|
||||
|
||||
[TestCase(0.5)]
|
||||
@ -202,14 +199,14 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddStep("adjust track rate", () => ((MasterGameplayClockContainer)Player.GameplayClockContainer).UserPlaybackRate.Value = rate);
|
||||
|
||||
addSeekStep(1000);
|
||||
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
|
||||
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
|
||||
AddAssert("progress almost same", () => drawableSpinner.Progress, () => Is.EqualTo(expectedProgress).Within(0.05));
|
||||
AddAssert("spm almost same", () => drawableSpinner.SpinsPerMinute.Value, () => Is.EqualTo(expectedSpm).Within(2.0));
|
||||
}
|
||||
|
||||
private void addSeekStep(double time)
|
||||
{
|
||||
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
|
||||
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
|
||||
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(100));
|
||||
}
|
||||
|
||||
private void transformReplay(Func<Replay, Replay> replayTransformation) => AddStep("set replay", () =>
|
||||
|
@ -2,10 +2,9 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.1" />
|
||||
<PackageReference Include="Moq" Version="4.18.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
public static class AimEvaluator
|
||||
{
|
||||
private const double wide_angle_multiplier = 1.5;
|
||||
private const double acute_angle_multiplier = 2.0;
|
||||
private const double slider_multiplier = 1.5;
|
||||
private const double acute_angle_multiplier = 1.95;
|
||||
private const double slider_multiplier = 1.35;
|
||||
private const double velocity_change_multiplier = 0.75;
|
||||
|
||||
/// <summary>
|
||||
@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
|
||||
}
|
||||
|
||||
if (osuLastObj.TravelTime != 0)
|
||||
if (osuLastObj.BaseObject is Slider)
|
||||
{
|
||||
// Reward sliders based on velocity.
|
||||
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;
|
||||
|
@ -15,11 +15,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
private const double max_opacity_bonus = 0.4;
|
||||
private const double hidden_bonus = 0.2;
|
||||
|
||||
private const double min_velocity = 0.5;
|
||||
private const double slider_multiplier = 1.3;
|
||||
|
||||
private const double min_angle_multiplier = 0.2;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the difficulty of memorising and hitting an object, based on:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>distance between the previous and current object,</description></item>
|
||||
/// <item><description>distance between a number of previous objects and the current object,</description></item>
|
||||
/// <item><description>the visual opacity of the current object,</description></item>
|
||||
/// <item><description>the angle made by the current object,</description></item>
|
||||
/// <item><description>length and speed of the current object (for sliders),</description></item>
|
||||
/// <item><description>and whether the hidden mod is enabled.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
@ -39,6 +46,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
|
||||
OsuDifficultyHitObject lastObj = osuCurrent;
|
||||
|
||||
double angleRepeatCount = 0.0;
|
||||
|
||||
// This is iterating backwards in time from the current object.
|
||||
for (int i = 0; i < Math.Min(current.Index, 10); i++)
|
||||
{
|
||||
@ -62,6 +71,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
double opacityBonus = 1.0 + max_opacity_bonus * (1.0 - osuCurrent.OpacityAt(currentHitObject.StartTime, hidden));
|
||||
|
||||
result += stackNerf * opacityBonus * scalingFactor * jumpDistance / cumulativeStrainTime;
|
||||
|
||||
if (currentObj.Angle != null && osuCurrent.Angle != null)
|
||||
{
|
||||
// Objects further back in time should count less for the nerf.
|
||||
if (Math.Abs(currentObj.Angle.Value - osuCurrent.Angle.Value) < 0.02)
|
||||
angleRepeatCount += Math.Max(1.0 - 0.1 * i, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
lastObj = currentObj;
|
||||
@ -73,6 +89,29 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
if (hidden)
|
||||
result *= 1.0 + hidden_bonus;
|
||||
|
||||
// Nerf patterns with repeated angles.
|
||||
result *= min_angle_multiplier + (1.0 - min_angle_multiplier) / (angleRepeatCount + 1.0);
|
||||
|
||||
double sliderBonus = 0.0;
|
||||
|
||||
if (osuCurrent.BaseObject is Slider osuSlider)
|
||||
{
|
||||
// Invert the scaling factor to determine the true travel distance independent of circle size.
|
||||
double pixelTravelDistance = osuSlider.LazyTravelDistance / scalingFactor;
|
||||
|
||||
// Reward sliders based on velocity.
|
||||
sliderBonus = Math.Pow(Math.Max(0.0, pixelTravelDistance / osuCurrent.TravelTime - min_velocity), 0.5);
|
||||
|
||||
// Longer sliders require more memorisation.
|
||||
sliderBonus *= pixelTravelDistance;
|
||||
|
||||
// Nerf sliders with repeats, as less memorisation is required.
|
||||
if (osuSlider.RepeatCount > 0)
|
||||
sliderBonus /= (osuSlider.RepeatCount + 1);
|
||||
}
|
||||
|
||||
result += sliderBonus * slider_multiplier;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
/// <summary>
|
||||
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, double greatWindow)
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||
{
|
||||
if (current.BaseObject is Spinner)
|
||||
return 0;
|
||||
@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
double lastDelta = lastObj.StrainTime;
|
||||
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
|
||||
|
||||
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
|
||||
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - currObj.HitWindowGreat * 0.3) / (currObj.HitWindowGreat * 0.3));
|
||||
|
||||
windowPenalty = Math.Min(1, windowPenalty);
|
||||
|
||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
/// <item><description>and how easily they can be cheesed.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, double greatWindow)
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||
{
|
||||
if (current.BaseObject is Spinner)
|
||||
return 0;
|
||||
@ -35,7 +35,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
var osuNextObj = (OsuDifficultyHitObject)current.Next(0);
|
||||
|
||||
double strainTime = osuCurrObj.StrainTime;
|
||||
double greatWindowFull = greatWindow * 2;
|
||||
double doubletapness = 1;
|
||||
|
||||
// Nerf doubletappable doubles.
|
||||
@ -45,13 +44,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
double nextDeltaTime = Math.Max(1, osuNextObj.DeltaTime);
|
||||
double deltaDifference = Math.Abs(nextDeltaTime - currDeltaTime);
|
||||
double speedRatio = currDeltaTime / Math.Max(currDeltaTime, deltaDifference);
|
||||
double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / greatWindowFull), 2);
|
||||
double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / osuCurrObj.HitWindowGreat), 2);
|
||||
doubletapness = Math.Pow(speedRatio, 1 - windowRatio);
|
||||
}
|
||||
|
||||
// Cap deltatime to the OD 300 hitwindow.
|
||||
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
||||
strainTime /= Math.Clamp((strainTime / greatWindowFull) / 0.93, 0.92, 1);
|
||||
strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindowGreat) / 0.93, 0.92, 1);
|
||||
|
||||
// derive speedBonus for calculation
|
||||
double speedBonus = 1.0;
|
||||
|
@ -23,9 +23,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
public class OsuDifficultyCalculator : DifficultyCalculator
|
||||
{
|
||||
private const double difficulty_multiplier = 0.0675;
|
||||
private double hitWindowGreat;
|
||||
|
||||
public override int Version => 20220701;
|
||||
public override int Version => 20220902;
|
||||
|
||||
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
@ -46,7 +45,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
|
||||
|
||||
if (mods.Any(h => h is OsuModRelax))
|
||||
{
|
||||
aimRating *= 0.9;
|
||||
speedRating = 0.0;
|
||||
flashlightRating *= 0.7;
|
||||
}
|
||||
|
||||
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
|
||||
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
|
||||
@ -62,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
|
||||
);
|
||||
|
||||
double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
|
||||
double starRating = basePerformance > 0.00001 ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
double drainRate = beatmap.Difficulty.DrainRate;
|
||||
@ -72,6 +75,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
||||
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
|
||||
|
||||
HitWindows hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
|
||||
return new OsuDifficultyAttributes
|
||||
{
|
||||
StarRating = starRating,
|
||||
@ -108,16 +116,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
{
|
||||
HitWindows hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
|
||||
return new Skill[]
|
||||
{
|
||||
new Aim(mods, true),
|
||||
new Aim(mods, false),
|
||||
new Speed(mods, hitWindowGreat),
|
||||
new Speed(mods),
|
||||
new Flashlight(mods)
|
||||
};
|
||||
}
|
||||
|
@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
public class OsuPerformanceCalculator : PerformanceCalculator
|
||||
{
|
||||
public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||
|
||||
private double accuracy;
|
||||
private int scoreMaxCombo;
|
||||
private int countGreat;
|
||||
@ -41,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
|
||||
|
||||
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||
double multiplier = PERFORMANCE_BASE_MULTIPLIER;
|
||||
|
||||
if (score.Mods.Any(m => m is OsuModNoFail))
|
||||
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
|
||||
@ -51,10 +53,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
if (score.Mods.Any(h => h is OsuModRelax))
|
||||
{
|
||||
// As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
|
||||
effectiveMissCount = Math.Min(effectiveMissCount + countOk + countMeh, totalHits);
|
||||
// https://www.desmos.com/calculator/bc9eybdthb
|
||||
// we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0
|
||||
// this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11)
|
||||
double okMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 1.8) : 1.0);
|
||||
double mehMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 5) : 1.0);
|
||||
|
||||
multiplier *= 0.6;
|
||||
// As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
|
||||
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
|
||||
}
|
||||
|
||||
double aimValue = computeAimValue(score, osuAttributes);
|
||||
@ -103,7 +109,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
if (attributes.ApproachRate > 10.33)
|
||||
approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
|
||||
else if (attributes.ApproachRate < 8.0)
|
||||
approachRateFactor = 0.1 * (8.0 - attributes.ApproachRate);
|
||||
approachRateFactor = 0.05 * (8.0 - attributes.ApproachRate);
|
||||
|
||||
if (score.Mods.Any(h => h is OsuModRelax))
|
||||
approachRateFactor = 0.0;
|
||||
|
||||
aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
|
||||
|
||||
@ -134,6 +143,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (score.Mods.Any(h => h is OsuModRelax))
|
||||
return 0.0;
|
||||
|
||||
double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||
|
||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||
@ -174,7 +186,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
|
||||
|
||||
// Scale the speed value with # of 50s to punish doubletapping.
|
||||
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
||||
speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
||||
|
||||
return speedValue;
|
||||
}
|
||||
@ -258,14 +270,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
||||
}
|
||||
|
||||
// Clamp miss count since it's derived from combo and can be higher than total hits and that breaks some calculations
|
||||
comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits);
|
||||
// Clamp miss count to maximum amount of possible breaks
|
||||
comboBasedMissCount = Math.Min(comboBasedMissCount, countOk + countMeh + countMiss);
|
||||
|
||||
return Math.Max(countMiss, comboBasedMissCount);
|
||||
}
|
||||
|
||||
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
|
||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
||||
}
|
||||
}
|
||||
|
@ -9,16 +9,21 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
{
|
||||
public class OsuDifficultyHitObject : DifficultyHitObject
|
||||
{
|
||||
private const int normalised_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
|
||||
/// <summary>
|
||||
/// A distance by which all distances should be scaled in order to assume a uniform circle size.
|
||||
/// </summary>
|
||||
public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
|
||||
|
||||
private const int min_delta_time = 25;
|
||||
private const float maximum_slider_radius = normalised_radius * 2.4f;
|
||||
private const float assumed_slider_radius = normalised_radius * 1.8f;
|
||||
private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f;
|
||||
private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f;
|
||||
|
||||
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
|
||||
|
||||
@ -64,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
public double TravelDistance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time taken to travel through <see cref="TravelDistance"/>, with a minimum value of 25ms for a non-zero distance.
|
||||
/// The time taken to travel through <see cref="TravelDistance"/>, with a minimum value of 25ms for <see cref="Slider"/> objects.
|
||||
/// </summary>
|
||||
public double TravelTime { get; private set; }
|
||||
|
||||
@ -74,6 +79,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
/// </summary>
|
||||
public double? Angle { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the full hit window for a Great <see cref="HitResult"/>.
|
||||
/// </summary>
|
||||
public double HitWindowGreat { get; private set; }
|
||||
|
||||
private readonly OsuHitObject lastLastObject;
|
||||
private readonly OsuHitObject lastObject;
|
||||
|
||||
@ -86,6 +96,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
||||
StrainTime = Math.Max(DeltaTime, min_delta_time);
|
||||
|
||||
if (BaseObject is Slider sliderObject)
|
||||
{
|
||||
HitWindowGreat = 2 * sliderObject.HeadCircle.HitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
}
|
||||
else
|
||||
{
|
||||
HitWindowGreat = 2 * BaseObject.HitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
}
|
||||
|
||||
setDistances(clockRate);
|
||||
}
|
||||
|
||||
@ -123,7 +142,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
if (BaseObject is Slider currentSlider)
|
||||
{
|
||||
computeSliderCursorPosition(currentSlider);
|
||||
TravelDistance = currentSlider.LazyTravelDistance;
|
||||
// Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
||||
TravelDistance = currentSlider.LazyTravelDistance * (float)Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5);
|
||||
TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time);
|
||||
}
|
||||
|
||||
@ -132,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
return;
|
||||
|
||||
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||
float scalingFactor = normalised_radius / (float)BaseObject.Radius;
|
||||
float scalingFactor = NORMALISED_RADIUS / (float)BaseObject.Radius;
|
||||
|
||||
if (BaseObject.Radius < 30)
|
||||
{
|
||||
@ -206,7 +226,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
|
||||
slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived.
|
||||
var currCursorPosition = slider.StackedPosition;
|
||||
double scalingFactor = normalised_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
|
||||
double scalingFactor = NORMALISED_RADIUS / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
|
||||
|
||||
for (int i = 1; i < slider.NestedHitObjects.Count; i++)
|
||||
{
|
||||
@ -234,7 +254,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
else if (currMovementObj is SliderRepeat)
|
||||
{
|
||||
// For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.
|
||||
requiredMovement = normalised_radius;
|
||||
requiredMovement = NORMALISED_RADIUS;
|
||||
}
|
||||
|
||||
if (currMovementLength > requiredMovement)
|
||||
@ -248,8 +268,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
if (i == slider.NestedHitObjects.Count - 1)
|
||||
slider.LazyEndPosition = currCursorPosition;
|
||||
}
|
||||
|
||||
slider.LazyTravelDistance *= (float)Math.Pow(1 + slider.RepeatCount / 2.5, 1.0 / 2.5); // Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
||||
}
|
||||
|
||||
private Vector2 getEndCursorPosition(OsuHitObject hitObject)
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
private double skillMultiplier => 23.25;
|
||||
private double skillMultiplier => 23.55;
|
||||
private double strainDecayBase => 0.15;
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
hasHiddenMod = mods.Any(m => m is OsuModHidden);
|
||||
}
|
||||
|
||||
private double skillMultiplier => 0.05;
|
||||
private double skillMultiplier => 0.052;
|
||||
private double strainDecayBase => 0.15;
|
||||
|
||||
private double currentStrain;
|
||||
|
@ -26,14 +26,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
|
||||
protected override int ReducedSectionCount => 5;
|
||||
protected override double DifficultyMultiplier => 1.04;
|
||||
private readonly double greatWindow;
|
||||
|
||||
private readonly List<double> objectStrains = new List<double>();
|
||||
|
||||
public Speed(Mod[] mods, double hitWindowGreat)
|
||||
public Speed(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
greatWindow = hitWindowGreat;
|
||||
}
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
@ -43,9 +41,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
protected override double StrainValueAt(DifficultyHitObject current)
|
||||
{
|
||||
currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime);
|
||||
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, greatWindow) * skillMultiplier;
|
||||
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier;
|
||||
|
||||
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current, greatWindow);
|
||||
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
|
||||
|
||||
double totalStrain = currentStrain * currentRhythm;
|
||||
|
||||
@ -64,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
if (maxStrain == 0)
|
||||
return 0;
|
||||
|
||||
return objectStrains.Aggregate((total, next) => total + (1.0 / (1.0 + Math.Exp(-(next / maxStrain * 12.0 - 6.0)))));
|
||||
return objectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@ -53,6 +54,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
private IBindable<Vector2> sliderPosition;
|
||||
private IBindable<float> sliderScale;
|
||||
|
||||
[UsedImplicitly]
|
||||
private readonly IBindable<int> sliderVersion;
|
||||
|
||||
public PathControlPointPiece(Slider slider, PathControlPoint controlPoint)
|
||||
{
|
||||
this.slider = slider;
|
||||
@ -61,11 +65,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
// we don't want to run the path type update on construction as it may inadvertently change the slider.
|
||||
cachePoints(slider);
|
||||
|
||||
slider.Path.Version.BindValueChanged(_ =>
|
||||
sliderVersion = slider.Path.Version.GetBoundCopy();
|
||||
|
||||
// schedule ensure that updates are only applied after all operations from a single frame are applied.
|
||||
// this avoids inadvertently changing the slider path type for batch operations.
|
||||
sliderVersion.BindValueChanged(_ => Scheduler.AddOnce(() =>
|
||||
{
|
||||
cachePoints(slider);
|
||||
updatePathType();
|
||||
});
|
||||
}));
|
||||
|
||||
controlPoint.Changed += updateMarkerDisplay;
|
||||
|
||||
|
@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
private InputManager inputManager;
|
||||
|
||||
public Action<List<PathControlPoint>> RemoveControlPointsRequested;
|
||||
public Action<List<PathControlPoint>> SplitControlPointsRequested;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider snapProvider { get; set; }
|
||||
@ -104,6 +105,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool splitSelected()
|
||||
{
|
||||
List<PathControlPoint> controlPointsToSplitAt = Pieces.Where(p => p.IsSelected.Value && isSplittable(p)).Select(p => p.ControlPoint).ToList();
|
||||
|
||||
// Ensure that there are any points to be split
|
||||
if (controlPointsToSplitAt.Count == 0)
|
||||
return false;
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
SplitControlPointsRequested?.Invoke(controlPointsToSplitAt);
|
||||
changeHandler?.EndChange();
|
||||
|
||||
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
|
||||
foreach (var piece in Pieces)
|
||||
piece.IsSelected.Value = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool isSplittable(PathControlPointPiece p) =>
|
||||
// A slider can only be split on control points which connect two different slider segments.
|
||||
p.ControlPoint.Type.HasValue && p != Pieces.FirstOrDefault() && p != Pieces.LastOrDefault();
|
||||
|
||||
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
switch (e.Action)
|
||||
@ -142,8 +166,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
foreach (var point in e.OldItems.Cast<PathControlPoint>())
|
||||
{
|
||||
Pieces.RemoveAll(p => p.ControlPoint == point);
|
||||
Connections.RemoveAll(c => c.ControlPoint == point);
|
||||
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
|
||||
piece.RemoveAndDisposeImmediately();
|
||||
foreach (var connection in Connections.Where(c => c.ControlPoint == point).ToArray())
|
||||
connection.RemoveAndDisposeImmediately();
|
||||
}
|
||||
|
||||
// If removing before the end of the path,
|
||||
@ -322,25 +348,42 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
if (count == 0)
|
||||
return null;
|
||||
|
||||
List<MenuItem> items = new List<MenuItem>();
|
||||
var splittablePieces = selectedPieces.Where(isSplittable).ToList();
|
||||
int splittableCount = splittablePieces.Count;
|
||||
|
||||
List<MenuItem> curveTypeItems = new List<MenuItem>();
|
||||
|
||||
if (!selectedPieces.Contains(Pieces[0]))
|
||||
items.Add(createMenuItemForPathType(null));
|
||||
curveTypeItems.Add(createMenuItemForPathType(null));
|
||||
|
||||
// todo: hide/disable items which aren't valid for selected points
|
||||
items.Add(createMenuItemForPathType(PathType.Linear));
|
||||
items.Add(createMenuItemForPathType(PathType.PerfectCurve));
|
||||
items.Add(createMenuItemForPathType(PathType.Bezier));
|
||||
items.Add(createMenuItemForPathType(PathType.Catmull));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.Linear));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.PerfectCurve));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.Bezier));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.Catmull));
|
||||
|
||||
return new MenuItem[]
|
||||
var menuItems = new List<MenuItem>
|
||||
{
|
||||
new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, () => DeleteSelected()),
|
||||
new OsuMenuItem("Curve type")
|
||||
{
|
||||
Items = items
|
||||
Items = curveTypeItems
|
||||
}
|
||||
};
|
||||
|
||||
if (splittableCount > 0)
|
||||
{
|
||||
menuItems.Add(new OsuMenuItem($"Split {"control point".ToQuantity(splittableCount, splittableCount > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}",
|
||||
MenuItemType.Destructive,
|
||||
() => splitSelected()));
|
||||
}
|
||||
|
||||
menuItems.Add(
|
||||
new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}",
|
||||
MenuItemType.Destructive,
|
||||
() => DeleteSelected())
|
||||
);
|
||||
|
||||
return menuItems.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,6 +89,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint();
|
||||
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
|
||||
|
||||
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.
|
||||
// Without re-applying defaults, velocity won't be updated.
|
||||
ApplyDefaultsToHitObject();
|
||||
break;
|
||||
|
||||
case SliderPlacementState.Body:
|
||||
|
@ -13,6 +13,7 @@ using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@ -111,7 +112,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(HitObject, true)
|
||||
{
|
||||
RemoveControlPointsRequested = removeControlPoints
|
||||
RemoveControlPointsRequested = removeControlPoints,
|
||||
SplitControlPointsRequested = splitControlPoints
|
||||
});
|
||||
|
||||
base.OnSelected();
|
||||
@ -249,15 +251,83 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
HitObject.Position += first;
|
||||
}
|
||||
|
||||
private void splitControlPoints(List<PathControlPoint> controlPointsToSplitAt)
|
||||
{
|
||||
// Arbitrary gap in milliseconds to put between split slider pieces
|
||||
const double split_gap = 100;
|
||||
|
||||
// Ensure that there are any points to be split
|
||||
if (controlPointsToSplitAt.Count == 0)
|
||||
return;
|
||||
|
||||
editorBeatmap.SelectedHitObjects.Clear();
|
||||
|
||||
foreach (var splitPoint in controlPointsToSplitAt)
|
||||
{
|
||||
if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type is null)
|
||||
continue;
|
||||
|
||||
// Split off the section of slider before this control point so the remaining control points to split are in the latter part of the slider.
|
||||
int index = controlPoints.IndexOf(splitPoint);
|
||||
|
||||
if (index <= 0)
|
||||
continue;
|
||||
|
||||
// Extract the split portion and remove from the original slider.
|
||||
var splitControlPoints = controlPoints.Take(index + 1).ToList();
|
||||
controlPoints.RemoveRange(0, index);
|
||||
|
||||
// Turn the control points which were split off into a new slider.
|
||||
var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone();
|
||||
var difficultyPoint = (DifficultyControlPoint)HitObject.DifficultyControlPoint.DeepClone();
|
||||
|
||||
var newSlider = new Slider
|
||||
{
|
||||
StartTime = HitObject.StartTime,
|
||||
Position = HitObject.Position + splitControlPoints[0].Position,
|
||||
NewCombo = HitObject.NewCombo,
|
||||
SampleControlPoint = samplePoint,
|
||||
DifficultyControlPoint = difficultyPoint,
|
||||
LegacyLastTickOffset = HitObject.LegacyLastTickOffset,
|
||||
Samples = HitObject.Samples.Select(s => s.With()).ToList(),
|
||||
RepeatCount = HitObject.RepeatCount,
|
||||
NodeSamples = HitObject.NodeSamples.Select(n => (IList<HitSampleInfo>)n.Select(s => s.With()).ToList()).ToList(),
|
||||
Path = new SliderPath(splitControlPoints.Select(o => new PathControlPoint(o.Position - splitControlPoints[0].Position, o == splitControlPoints[^1] ? null : o.Type)).ToArray())
|
||||
};
|
||||
|
||||
// Increase the start time of the slider before adding the new slider so the new slider is immediately inserted at the correct index and internal state remains valid.
|
||||
HitObject.StartTime += split_gap;
|
||||
|
||||
editorBeatmap.Add(newSlider);
|
||||
|
||||
HitObject.NewCombo = false;
|
||||
HitObject.Path.ExpectedDistance.Value -= newSlider.Path.CalculatedDistance;
|
||||
HitObject.StartTime += newSlider.SpanDuration;
|
||||
|
||||
// In case the remainder of the slider has no length left over, give it length anyways so we don't get a 0 length slider.
|
||||
if (HitObject.Path.ExpectedDistance.Value <= Precision.DOUBLE_EPSILON)
|
||||
{
|
||||
HitObject.Path.ExpectedDistance.Value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Once all required pieces have been split off, the original slider has the final split.
|
||||
// As a final step, we must reset its control points to have an origin of (0,0).
|
||||
Vector2 first = controlPoints[0].Position;
|
||||
foreach (var c in controlPoints)
|
||||
c.Position -= first;
|
||||
HitObject.Position += first;
|
||||
}
|
||||
|
||||
private void convertToStream()
|
||||
{
|
||||
if (editorBeatmap == null || changeHandler == null || beatDivisor == null)
|
||||
if (editorBeatmap == null || beatDivisor == null)
|
||||
return;
|
||||
|
||||
var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(HitObject.StartTime);
|
||||
double streamSpacing = timingPoint.BeatLength / beatDivisor.Value;
|
||||
|
||||
changeHandler.BeginChange();
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
int i = 0;
|
||||
double time = HitObject.StartTime;
|
||||
@ -292,7 +362,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
editorBeatmap.Remove(HitObject);
|
||||
|
||||
changeHandler.EndChange();
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
|
||||
public override MenuItem[] ContextMenuItems => new MenuItem[]
|
||||
|
@ -6,8 +6,11 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@ -15,6 +18,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
@ -53,6 +57,17 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
referencePathTypes = null;
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed)
|
||||
{
|
||||
mergeSelection();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
@ -112,11 +127,11 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
didFlip = true;
|
||||
|
||||
foreach (var point in slider.Path.ControlPoints)
|
||||
foreach (var cp in slider.Path.ControlPoints)
|
||||
{
|
||||
point.Position = new Vector2(
|
||||
(direction == Direction.Horizontal ? -1 : 1) * point.Position.X,
|
||||
(direction == Direction.Vertical ? -1 : 1) * point.Position.Y
|
||||
cp.Position = new Vector2(
|
||||
(direction == Direction.Horizontal ? -1 : 1) * cp.Position.X,
|
||||
(direction == Direction.Vertical ? -1 : 1) * cp.Position.Y
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -168,8 +183,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
if (h is IHasPath path)
|
||||
{
|
||||
foreach (var point in path.Path.ControlPoints)
|
||||
point.Position = RotatePointAroundOrigin(point.Position, Vector2.Zero, delta);
|
||||
foreach (PathControlPoint cp in path.Path.ControlPoints)
|
||||
cp.Position = RotatePointAroundOrigin(cp.Position, Vector2.Zero, delta);
|
||||
}
|
||||
}
|
||||
|
||||
@ -320,7 +335,115 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
/// All osu! hitobjects which can be moved/rotated/scaled.
|
||||
/// </summary>
|
||||
private OsuHitObject[] selectedMovableObjects => SelectedItems.OfType<OsuHitObject>()
|
||||
.Where(h => !(h is Spinner))
|
||||
.Where(h => h is not Spinner)
|
||||
.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// All osu! hitobjects which can be merged.
|
||||
/// </summary>
|
||||
private OsuHitObject[] selectedMergeableObjects => SelectedItems.OfType<OsuHitObject>()
|
||||
.Where(h => h is HitCircle or Slider)
|
||||
.OrderBy(h => h.StartTime)
|
||||
.ToArray();
|
||||
|
||||
private void mergeSelection()
|
||||
{
|
||||
var mergeableObjects = selectedMergeableObjects;
|
||||
|
||||
if (!canMerge(mergeableObjects))
|
||||
return;
|
||||
|
||||
EditorBeatmap.BeginChange();
|
||||
|
||||
// Have an initial slider object.
|
||||
var firstHitObject = mergeableObjects[0];
|
||||
var mergedHitObject = firstHitObject as Slider ?? new Slider
|
||||
{
|
||||
StartTime = firstHitObject.StartTime,
|
||||
Position = firstHitObject.Position,
|
||||
NewCombo = firstHitObject.NewCombo,
|
||||
SampleControlPoint = firstHitObject.SampleControlPoint,
|
||||
Samples = firstHitObject.Samples,
|
||||
};
|
||||
|
||||
if (mergedHitObject.Path.ControlPoints.Count == 0)
|
||||
{
|
||||
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(Vector2.Zero, PathType.Linear));
|
||||
}
|
||||
|
||||
// Merge all the selected hit objects into one slider path.
|
||||
bool lastCircle = firstHitObject is HitCircle;
|
||||
|
||||
foreach (var selectedMergeableObject in mergeableObjects.Skip(1))
|
||||
{
|
||||
if (selectedMergeableObject is IHasPath hasPath)
|
||||
{
|
||||
var offset = lastCircle ? selectedMergeableObject.Position - mergedHitObject.Position : mergedHitObject.Path.ControlPoints[^1].Position;
|
||||
float distanceToLastControlPoint = Vector2.Distance(mergedHitObject.Path.ControlPoints[^1].Position, offset);
|
||||
|
||||
// Calculate the distance required to travel to the expected distance of the merging slider.
|
||||
mergedHitObject.Path.ExpectedDistance.Value = mergedHitObject.Path.CalculatedDistance + distanceToLastControlPoint + hasPath.Path.Distance;
|
||||
|
||||
// Remove the last control point if it sits exactly on the start of the next control point.
|
||||
if (Precision.AlmostEquals(distanceToLastControlPoint, 0))
|
||||
{
|
||||
mergedHitObject.Path.ControlPoints.RemoveAt(mergedHitObject.Path.ControlPoints.Count - 1);
|
||||
}
|
||||
|
||||
mergedHitObject.Path.ControlPoints.AddRange(hasPath.Path.ControlPoints.Select(o => new PathControlPoint(o.Position + offset, o.Type)));
|
||||
lastCircle = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Turn the last control point into a linear type if this is the first merging circle in a sequence, so the subsequent control points can be inherited path type.
|
||||
if (!lastCircle)
|
||||
{
|
||||
mergedHitObject.Path.ControlPoints.Last().Type = PathType.Linear;
|
||||
}
|
||||
|
||||
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(selectedMergeableObject.Position - mergedHitObject.Position));
|
||||
mergedHitObject.Path.ExpectedDistance.Value = null;
|
||||
lastCircle = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure only the merged hit object is in the beatmap.
|
||||
if (firstHitObject is Slider)
|
||||
{
|
||||
foreach (var selectedMergeableObject in mergeableObjects.Skip(1))
|
||||
{
|
||||
EditorBeatmap.Remove(selectedMergeableObject);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var selectedMergeableObject in mergeableObjects)
|
||||
{
|
||||
EditorBeatmap.Remove(selectedMergeableObject);
|
||||
}
|
||||
|
||||
EditorBeatmap.Add(mergedHitObject);
|
||||
}
|
||||
|
||||
// Make sure the merged hitobject is selected.
|
||||
SelectedItems.Clear();
|
||||
SelectedItems.Add(mergedHitObject);
|
||||
|
||||
EditorBeatmap.EndChange();
|
||||
}
|
||||
|
||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
|
||||
{
|
||||
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
||||
yield return item;
|
||||
|
||||
if (canMerge(selectedMergeableObjects))
|
||||
yield return new OsuMenuItem("Merge selection", MenuItemType.Destructive, mergeSelection);
|
||||
}
|
||||
|
||||
private bool canMerge(IReadOnlyList<OsuHitObject> objects) =>
|
||||
objects.Count > 1
|
||||
&& (objects.Any(h => h is Slider)
|
||||
|| objects.Zip(objects.Skip(1), (h1, h2) => Precision.DefinitelyBigger(Vector2.DistanceSquared(h1.Position, h2.Position), 1)).Any(x => x));
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
||||
private void updateBeatmap()
|
||||
{
|
||||
Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value;
|
||||
Beatmap.SaveState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset<OsuHitObject>
|
||||
public abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IUpdatableByPlayfield
|
||||
{
|
||||
public override double ScoreMultiplier => 1.0;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(OsuModCinema) };
|
||||
@ -62,15 +62,18 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
gameplayClock = drawableRuleset.FrameStableClock;
|
||||
}
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
if (LastAcceptedAction != null && nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
|
||||
LastAcceptedAction = null;
|
||||
}
|
||||
|
||||
protected abstract bool CheckValidNewAction(OsuAction action);
|
||||
|
||||
private bool checkCorrectAction(OsuAction action)
|
||||
{
|
||||
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
|
||||
{
|
||||
LastAcceptedAction = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (action)
|
||||
{
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public override string Name => @"Alternate";
|
||||
public override string Acronym => @"AL";
|
||||
public override string Description => @"Don't use the same key twice in a row!";
|
||||
public override LocalisableString Description => @"Don't use the same key twice in a row!";
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();
|
||||
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public override string Name => "Approach Different";
|
||||
public override string Acronym => "AD";
|
||||
public override string Description => "Never trust the approach circles...";
|
||||
public override LocalisableString Description => "Never trust the approach circles...";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
|
||||
|
||||
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.StateChanges;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@ -20,8 +21,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override string Acronym => "AP";
|
||||
public override IconUsage? Icon => OsuIcon.ModAutopilot;
|
||||
public override ModType Type => ModType.Automation;
|
||||
public override string Description => @"Automatic cursor movement - just follow the rhythm.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override LocalisableString Description => @"Automatic cursor movement - just follow the rhythm.";
|
||||
public override double ScoreMultiplier => 0.1;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) };
|
||||
|
||||
public bool PerformFail() => false;
|
||||
@ -30,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
private OsuInputManager inputManager = null!;
|
||||
|
||||
private IFrameStableClock gameplayClock = null!;
|
||||
|
||||
private List<OsuReplayFrame> replayFrames = null!;
|
||||
|
||||
private int currentFrame;
|
||||
@ -40,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
if (currentFrame == replayFrames.Count - 1) return;
|
||||
|
||||
double time = gameplayClock.CurrentTime;
|
||||
double time = playfield.Clock.CurrentTime;
|
||||
|
||||
// Very naive implementation of autopilot based on proximity to replay frames.
|
||||
// TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered).
|
||||
@ -55,8 +54,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
gameplayClock = drawableRuleset.FrameStableClock;
|
||||
|
||||
// Grab the input manager to disable the user's cursor, and for future use
|
||||
inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
|
||||
inputManager.AllowUserCursorMovement = false;
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public class OsuModBlinds : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToHealthProcessor
|
||||
{
|
||||
public override string Name => "Blinds";
|
||||
public override string Description => "Play with blinds on your screen.";
|
||||
public override LocalisableString Description => "Play with blinds on your screen.";
|
||||
public override string Acronym => "BL";
|
||||
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Adjust;
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public override IconUsage? Icon => FontAwesome.Solid.CompressArrowsAlt;
|
||||
|
||||
public override string Description => "Hit them at the right size!";
|
||||
public override LocalisableString Description => "Hit them at the right size!";
|
||||
|
||||
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
|
||||
public override BindableNumber<float> StartScale { get; } = new BindableFloat
|
||||
|
@ -1,12 +1,13 @@
|
||||
// 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.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModEasy : ModEasyWithExtraLives
|
||||
{
|
||||
public override string Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!";
|
||||
public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!";
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user