Merge branch 'master' into fix-test-beatmap-virtual-clock
@ -2,7 +2,7 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectModuleManager">
|
<component name="ProjectModuleManager">
|
||||||
<modules>
|
<modules>
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/riderModule.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/riderModule.iml" />
|
||||||
</modules>
|
</modules>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
@ -34,6 +34,8 @@ If you are looking to install or test osu! without setting up a development envi
|
|||||||
| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS(iOS 10+)](https://osu.ppy.sh/home/testflight) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
|
| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS(iOS 10+)](https://osu.ppy.sh/home/testflight) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
|
||||||
| ------------- | ------------- | ------------- | ------------- | ------------- |
|
| ------------- | ------------- | ------------- | ------------- | ------------- |
|
||||||
|
|
||||||
|
- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.
|
||||||
|
|
||||||
- When running on Windows 7 or 8.1, **[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/dependencies?tabs=netcore31&pivots=os-windows)** may be required to correctly run .NET Core applications if your operating system is not up-to-date with the latest service packs.
|
- When running on Windows 7 or 8.1, **[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/dependencies?tabs=netcore31&pivots=os-windows)** may be required to correctly run .NET Core applications if your operating system is not up-to-date with the latest service packs.
|
||||||
|
|
||||||
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
|
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
<Reference Include="Java.Interop" />
|
<Reference Include="Java.Interop" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1016.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1004.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1019.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
34
osu.Android/GameplayScreenRotationLocker.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using Android.Content.PM;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game;
|
||||||
|
|
||||||
|
namespace osu.Android
|
||||||
|
{
|
||||||
|
public class GameplayScreenRotationLocker : Component
|
||||||
|
{
|
||||||
|
private Bindable<bool> localUserPlaying;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuGameActivity gameActivity { get; set; }
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuGame game)
|
||||||
|
{
|
||||||
|
localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
|
||||||
|
localUserPlaying.BindValueChanged(updateLock, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLock(ValueChangedEvent<bool> userPlaying)
|
||||||
|
{
|
||||||
|
gameActivity.RunOnUiThread(() =>
|
||||||
|
{
|
||||||
|
gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : ScreenOrientation.FullUser;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,7 @@ namespace osu.Android
|
|||||||
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
|
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
|
||||||
public class OsuGameActivity : AndroidGameActivity
|
public class OsuGameActivity : AndroidGameActivity
|
||||||
{
|
{
|
||||||
protected override Framework.Game CreateGame() => new OsuGameAndroid();
|
protected override Framework.Game CreateGame() => new OsuGameAndroid(this);
|
||||||
|
|
||||||
protected override void OnCreate(Bundle savedInstanceState)
|
protected override void OnCreate(Bundle savedInstanceState)
|
||||||
{
|
{
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using Android.App;
|
using Android.App;
|
||||||
using Android.OS;
|
using Android.OS;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Game;
|
using osu.Game;
|
||||||
using osu.Game.Updater;
|
using osu.Game.Updater;
|
||||||
|
|
||||||
@ -11,6 +12,15 @@ namespace osu.Android
|
|||||||
{
|
{
|
||||||
public class OsuGameAndroid : OsuGame
|
public class OsuGameAndroid : OsuGame
|
||||||
{
|
{
|
||||||
|
[Cached]
|
||||||
|
private readonly OsuGameActivity gameActivity;
|
||||||
|
|
||||||
|
public OsuGameAndroid(OsuGameActivity activity)
|
||||||
|
: base(null)
|
||||||
|
{
|
||||||
|
gameActivity = activity;
|
||||||
|
}
|
||||||
|
|
||||||
public override Version AssemblyVersion
|
public override Version AssemblyVersion
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@ -55,6 +65,12 @@ namespace osu.Android
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
LoadComponentAsync(new GameplayScreenRotationLocker(), Add);
|
||||||
|
}
|
||||||
|
|
||||||
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
|
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
<AndroidLinkTool>r8</AndroidLinkTool>
|
<AndroidLinkTool>r8</AndroidLinkTool>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="GameplayScreenRotationLocker.cs" />
|
||||||
<Compile Include="OsuGameActivity.cs" />
|
<Compile Include="OsuGameActivity.cs" />
|
||||||
<Compile Include="OsuGameAndroid.cs" />
|
<Compile Include="OsuGameAndroid.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@ -53,4 +54,4 @@
|
|||||||
<AndroidResource Include="Resources\drawable\lazer.png" />
|
<AndroidResource Include="Resources\drawable\lazer.png" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
||||||
</Project>
|
</Project>
|
@ -125,12 +125,14 @@ namespace osu.Desktop
|
|||||||
{
|
{
|
||||||
base.SetHost(host);
|
base.SetHost(host);
|
||||||
|
|
||||||
|
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
||||||
|
|
||||||
switch (host.Window)
|
switch (host.Window)
|
||||||
{
|
{
|
||||||
// Legacy osuTK DesktopGameWindow
|
// Legacy osuTK DesktopGameWindow
|
||||||
case DesktopGameWindow desktopGameWindow:
|
case DesktopGameWindow desktopGameWindow:
|
||||||
desktopGameWindow.CursorState |= CursorState.Hidden;
|
desktopGameWindow.CursorState |= CursorState.Hidden;
|
||||||
desktopGameWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"));
|
desktopGameWindow.SetIconFromStream(iconStream);
|
||||||
desktopGameWindow.Title = Name;
|
desktopGameWindow.Title = Name;
|
||||||
desktopGameWindow.FileDrop += (_, e) => fileDrop(e.FileNames);
|
desktopGameWindow.FileDrop += (_, e) => fileDrop(e.FileNames);
|
||||||
break;
|
break;
|
||||||
@ -138,6 +140,7 @@ namespace osu.Desktop
|
|||||||
// SDL2 DesktopWindow
|
// SDL2 DesktopWindow
|
||||||
case DesktopWindow desktopWindow:
|
case DesktopWindow desktopWindow:
|
||||||
desktopWindow.CursorState.Value |= CursorState.Hidden;
|
desktopWindow.CursorState.Value |= CursorState.Hidden;
|
||||||
|
desktopWindow.SetIconFromStream(iconStream);
|
||||||
desktopWindow.Title = Name;
|
desktopWindow.Title = Name;
|
||||||
desktopWindow.DragDrop += f => fileDrop(new[] { f });
|
desktopWindow.DragDrop += f => fileDrop(new[] { f });
|
||||||
break;
|
break;
|
||||||
|
@ -29,6 +29,11 @@ namespace osu.Desktop.Updater
|
|||||||
|
|
||||||
private static readonly Logger logger = Logger.GetLogger("updater");
|
private static readonly Logger logger = Logger.GetLogger("updater");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether an update has been downloaded but not yet applied.
|
||||||
|
/// </summary>
|
||||||
|
private bool updatePending;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(NotificationOverlay notification)
|
private void load(NotificationOverlay notification)
|
||||||
{
|
{
|
||||||
@ -37,9 +42,9 @@ namespace osu.Desktop.Updater
|
|||||||
Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
|
Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task PerformUpdateCheck() => await checkForUpdateAsync();
|
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync();
|
||||||
|
|
||||||
private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
|
private async Task<bool> checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
|
||||||
{
|
{
|
||||||
// should we schedule a retry on completion of this check?
|
// should we schedule a retry on completion of this check?
|
||||||
bool scheduleRecheck = true;
|
bool scheduleRecheck = true;
|
||||||
@ -49,9 +54,19 @@ namespace osu.Desktop.Updater
|
|||||||
updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true);
|
updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true);
|
||||||
|
|
||||||
var info = await updateManager.CheckForUpdate(!useDeltaPatching);
|
var info = await updateManager.CheckForUpdate(!useDeltaPatching);
|
||||||
|
|
||||||
if (info.ReleasesToApply.Count == 0)
|
if (info.ReleasesToApply.Count == 0)
|
||||||
|
{
|
||||||
|
if (updatePending)
|
||||||
|
{
|
||||||
|
// the user may have dismissed the completion notice, so show it again.
|
||||||
|
notificationOverlay.Post(new UpdateCompleteNotification(this));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// no updates available. bail and retry later.
|
// no updates available. bail and retry later.
|
||||||
return;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (notification == null)
|
if (notification == null)
|
||||||
{
|
{
|
||||||
@ -72,6 +87,7 @@ namespace osu.Desktop.Updater
|
|||||||
await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f);
|
await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f);
|
||||||
|
|
||||||
notification.State = ProgressNotificationState.Completed;
|
notification.State = ProgressNotificationState.Completed;
|
||||||
|
updatePending = true;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -103,6 +119,8 @@ namespace osu.Desktop.Updater
|
|||||||
Scheduler.AddDelayed(async () => await checkForUpdateAsync(), 60000 * 30);
|
Scheduler.AddDelayed(async () => await checkForUpdateAsync(), 60000 * 30);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
@ -111,10 +129,27 @@ namespace osu.Desktop.Updater
|
|||||||
updateManager?.Dispose();
|
updateManager?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class UpdateCompleteNotification : ProgressCompletionNotification
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private OsuGame game { get; set; }
|
||||||
|
|
||||||
|
public UpdateCompleteNotification(SquirrelUpdateManager updateManager)
|
||||||
|
{
|
||||||
|
Text = @"Update ready to install. Click to restart!";
|
||||||
|
|
||||||
|
Activated = () =>
|
||||||
|
{
|
||||||
|
updateManager.PrepareUpdateAsync()
|
||||||
|
.ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class UpdateProgressNotification : ProgressNotification
|
private class UpdateProgressNotification : ProgressNotification
|
||||||
{
|
{
|
||||||
private readonly SquirrelUpdateManager updateManager;
|
private readonly SquirrelUpdateManager updateManager;
|
||||||
private OsuGame game;
|
|
||||||
|
|
||||||
public UpdateProgressNotification(SquirrelUpdateManager updateManager)
|
public UpdateProgressNotification(SquirrelUpdateManager updateManager)
|
||||||
{
|
{
|
||||||
@ -123,23 +158,12 @@ namespace osu.Desktop.Updater
|
|||||||
|
|
||||||
protected override Notification CreateCompletionNotification()
|
protected override Notification CreateCompletionNotification()
|
||||||
{
|
{
|
||||||
return new ProgressCompletionNotification
|
return new UpdateCompleteNotification(updateManager);
|
||||||
{
|
|
||||||
Text = @"Update ready to install. Click to restart!",
|
|
||||||
Activated = () =>
|
|
||||||
{
|
|
||||||
updateManager.PrepareUpdateAsync()
|
|
||||||
.ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours, OsuGame game)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
this.game = game;
|
|
||||||
|
|
||||||
IconContent.AddRange(new Drawable[]
|
IconContent.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
new Box
|
new Box
|
||||||
|
@ -5,24 +5,24 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
|
using osu.Game;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
|
|
||||||
namespace osu.Desktop.Windows
|
namespace osu.Desktop.Windows
|
||||||
{
|
{
|
||||||
public class GameplayWinKeyBlocker : Component
|
public class GameplayWinKeyBlocker : Component
|
||||||
{
|
{
|
||||||
private Bindable<bool> allowScreenSuspension;
|
|
||||||
private Bindable<bool> disableWinKey;
|
private Bindable<bool> disableWinKey;
|
||||||
|
private Bindable<bool> localUserPlaying;
|
||||||
|
|
||||||
private GameHost host;
|
[Resolved]
|
||||||
|
private GameHost host { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(GameHost host, OsuConfigManager config)
|
private void load(OsuGame game, OsuConfigManager config)
|
||||||
{
|
{
|
||||||
this.host = host;
|
localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
|
||||||
|
localUserPlaying.BindValueChanged(_ => updateBlocking());
|
||||||
allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy();
|
|
||||||
allowScreenSuspension.BindValueChanged(_ => updateBlocking());
|
|
||||||
|
|
||||||
disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey);
|
disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey);
|
||||||
disableWinKey.BindValueChanged(_ => updateBlocking(), true);
|
disableWinKey.BindValueChanged(_ => updateBlocking(), true);
|
||||||
@ -30,7 +30,7 @@ namespace osu.Desktop.Windows
|
|||||||
|
|
||||||
private void updateBlocking()
|
private void updateBlocking()
|
||||||
{
|
{
|
||||||
bool shouldDisable = disableWinKey.Value && !allowScreenSuspension.Value;
|
bool shouldDisable = disableWinKey.Value && localUserPlaying.Value;
|
||||||
|
|
||||||
if (shouldDisable)
|
if (shouldDisable)
|
||||||
host.InputThread.Scheduler.Add(WindowsKey.Disable);
|
host.InputThread.Scheduler.Add(WindowsKey.Disable);
|
||||||
|
@ -123,7 +123,10 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Scale = new Vector2(4f),
|
Scale = new Vector2(4f),
|
||||||
}, skin);
|
}, skin);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("get trails container", () =>
|
||||||
|
{
|
||||||
trails = catcherArea.OfType<CatcherTrailDisplay>().Single();
|
trails = catcherArea.OfType<CatcherTrailDisplay>().Single();
|
||||||
catcherArea.MovableCatcher.SetHyperDashState(2);
|
catcherArea.MovableCatcher.SetHyperDashState(2);
|
||||||
});
|
});
|
||||||
|
@ -141,11 +141,40 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
|
|
||||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch };
|
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch };
|
||||||
|
|
||||||
|
protected override IEnumerable<HitResult> GetValidHitResults()
|
||||||
|
{
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
HitResult.Great,
|
||||||
|
|
||||||
|
HitResult.LargeTickHit,
|
||||||
|
HitResult.SmallTickHit,
|
||||||
|
HitResult.LargeBonus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string GetDisplayNameForHitResult(HitResult result)
|
||||||
|
{
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case HitResult.LargeTickHit:
|
||||||
|
return "large droplet";
|
||||||
|
|
||||||
|
case HitResult.SmallTickHit:
|
||||||
|
return "small droplet";
|
||||||
|
|
||||||
|
case HitResult.LargeBonus:
|
||||||
|
return "banana";
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.GetDisplayNameForHitResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
|
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
|
||||||
|
|
||||||
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);
|
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);
|
||||||
|
|
||||||
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new CatchPerformanceCalculator(this, beatmap, score);
|
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score);
|
||||||
|
|
||||||
public int LegacyID => 2;
|
public int LegacyID => 2;
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@ -25,8 +24,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
private int tinyTicksMissed;
|
private int tinyTicksMissed;
|
||||||
private int misses;
|
private int misses;
|
||||||
|
|
||||||
public CatchPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score)
|
public CatchPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
|
||||||
: base(ruleset, beatmap, score)
|
: base(ruleset, attributes, score)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -56,6 +57,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
Volume = s.Volume
|
Volume = s.Volume
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
|
int nodeIndex = 0;
|
||||||
SliderEventDescriptor? lastEvent = null;
|
SliderEventDescriptor? lastEvent = null;
|
||||||
|
|
||||||
foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
|
foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
|
||||||
@ -105,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
case SliderEventType.Repeat:
|
case SliderEventType.Repeat:
|
||||||
AddNested(new Fruit
|
AddNested(new Fruit
|
||||||
{
|
{
|
||||||
Samples = Samples,
|
Samples = this.GetNodeSamples(nodeIndex++),
|
||||||
StartTime = e.Time,
|
StartTime = e.Time,
|
||||||
X = X + Path.PositionAt(e.PathProgress).X,
|
X = X + Path.PositionAt(e.PathProgress).X,
|
||||||
});
|
});
|
||||||
@ -119,7 +121,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
public double Duration
|
public double Duration
|
||||||
{
|
{
|
||||||
get => this.SpanCount() * Path.Distance / Velocity;
|
get => this.SpanCount() * Path.Distance / Velocity;
|
||||||
set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
|
set => throw new NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
|
||||||
}
|
}
|
||||||
|
|
||||||
public double EndTime => StartTime + Duration;
|
public double EndTime => StartTime + Duration;
|
||||||
|
@ -13,6 +13,11 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
|||||||
{
|
{
|
||||||
public class CatchLegacySkinTransformer : LegacySkinTransformer
|
public class CatchLegacySkinTransformer : LegacySkinTransformer
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
|
||||||
|
/// </summary>
|
||||||
|
private bool providesComboCounter => this.HasFont(GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score");
|
||||||
|
|
||||||
public CatchLegacySkinTransformer(ISkinSource source)
|
public CatchLegacySkinTransformer(ISkinSource source)
|
||||||
: base(source)
|
: base(source)
|
||||||
{
|
{
|
||||||
@ -20,6 +25,16 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
|||||||
|
|
||||||
public override Drawable GetDrawableComponent(ISkinComponent component)
|
public override Drawable GetDrawableComponent(ISkinComponent component)
|
||||||
{
|
{
|
||||||
|
if (component is HUDSkinComponent hudComponent)
|
||||||
|
{
|
||||||
|
switch (hudComponent.Component)
|
||||||
|
{
|
||||||
|
case HUDSkinComponents.ComboCounter:
|
||||||
|
// catch may provide its own combo counter; hide the default.
|
||||||
|
return providesComboCounter ? Drawable.Empty() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!(component is CatchSkinComponent catchSkinComponent))
|
if (!(component is CatchSkinComponent catchSkinComponent))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@ -55,11 +70,9 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
|||||||
this.GetAnimation("fruit-ryuuta", true, true, true);
|
this.GetAnimation("fruit-ryuuta", true, true, true);
|
||||||
|
|
||||||
case CatchSkinComponents.CatchComboCounter:
|
case CatchSkinComponents.CatchComboCounter:
|
||||||
var comboFont = GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
|
|
||||||
|
|
||||||
// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
|
if (providesComboCounter)
|
||||||
if (this.HasFont(comboFont))
|
return new LegacyCatchComboCounter(Source);
|
||||||
return new LegacyComboCounter(Source);
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
|
/// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter
|
public class LegacyCatchComboCounter : CompositeDrawable, ICatchComboCounter
|
||||||
{
|
{
|
||||||
private readonly LegacyRollingCounter counter;
|
private readonly LegacyRollingCounter counter;
|
||||||
|
|
||||||
private readonly LegacyRollingCounter explosion;
|
private readonly LegacyRollingCounter explosion;
|
||||||
|
|
||||||
public LegacyComboCounter(ISkin skin)
|
public LegacyCatchComboCounter(ISkin skin)
|
||||||
{
|
{
|
||||||
var fontName = skin.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
|
var fontName = skin.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
|
||||||
var fontOverlap = skin.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? -2f;
|
var fontOverlap = skin.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? -2f;
|
@ -145,11 +145,19 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
trailsTarget.Add(trails = new CatcherTrailDisplay(this));
|
trails = new CatcherTrailDisplay(this);
|
||||||
|
|
||||||
updateCatcher();
|
updateCatcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
// don't add in above load as we may potentially modify a parent in an unsafe manner.
|
||||||
|
trailsTarget.Add(trails);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates proxied content to be displayed beneath hitobjects.
|
/// Creates proxied content to be displayed beneath hitobjects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -83,11 +83,17 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
RandomZ = snapshot.RandomZ;
|
RandomZ = snapshot.RandomZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void PostProcess()
|
||||||
|
{
|
||||||
|
base.PostProcess();
|
||||||
|
Objects.Sort();
|
||||||
|
}
|
||||||
|
|
||||||
public bool Equals(ManiaConvertMapping other) => other != null && RandomW == other.RandomW && RandomX == other.RandomX && RandomY == other.RandomY && RandomZ == other.RandomZ;
|
public bool Equals(ManiaConvertMapping other) => other != null && RandomW == other.RandomW && RandomX == other.RandomX && RandomY == other.RandomY && RandomZ == other.RandomZ;
|
||||||
public override bool Equals(ConvertMapping<ConvertValue> other) => base.Equals(other) && Equals(other as ManiaConvertMapping);
|
public override bool Equals(ConvertMapping<ConvertValue> other) => base.Equals(other) && Equals(other as ManiaConvertMapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ConvertValue : IEquatable<ConvertValue>
|
public struct ConvertValue : IEquatable<ConvertValue>, IComparable<ConvertValue>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A sane value to account for osu!stable using ints everwhere.
|
/// A sane value to account for osu!stable using ints everwhere.
|
||||||
@ -102,5 +108,15 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
|
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
|
||||||
&& Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
|
&& Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
|
||||||
&& Column == other.Column;
|
&& Column == other.Column;
|
||||||
|
|
||||||
|
public int CompareTo(ConvertValue other)
|
||||||
|
{
|
||||||
|
var result = StartTime.CompareTo(other.StartTime);
|
||||||
|
|
||||||
|
if (result != 0)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
return Column.CompareTo(other.Column);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
|
||||||
|
|
||||||
[TestCase(2.3683365342338796d, "diffcalc-test")]
|
[TestCase(2.3449735700206298d, "diffcalc-test")]
|
||||||
public void Test(double expected, string name)
|
public void Test(double expected, string name)
|
||||||
=> base.Test(expected, name);
|
=> base.Test(expected, name);
|
||||||
|
|
||||||
|
@ -21,13 +21,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int TotalColumns => Stages.Sum(g => g.Columns);
|
public int TotalColumns => Stages.Sum(g => g.Columns);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The total number of columns that were present in this <see cref="ManiaBeatmap"/> before any user adjustments.
|
||||||
|
/// </summary>
|
||||||
|
public readonly int OriginalTotalColumns;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="ManiaBeatmap"/>.
|
/// Creates a new <see cref="ManiaBeatmap"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="defaultStage">The initial stages.</param>
|
/// <param name="defaultStage">The initial stages.</param>
|
||||||
public ManiaBeatmap(StageDefinition defaultStage)
|
/// <param name="originalTotalColumns">The total number of columns present before any user adjustments. Defaults to the total columns in <paramref name="defaultStage"/>.</param>
|
||||||
|
public ManiaBeatmap(StageDefinition defaultStage, int? originalTotalColumns = null)
|
||||||
{
|
{
|
||||||
Stages.Add(defaultStage);
|
Stages.Add(defaultStage);
|
||||||
|
OriginalTotalColumns = originalTotalColumns ?? defaultStage.Columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<BeatmapStatistic> GetStatistics()
|
public override IEnumerable<BeatmapStatistic> GetStatistics()
|
||||||
|
@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
|||||||
public bool Dual;
|
public bool Dual;
|
||||||
public readonly bool IsForCurrentRuleset;
|
public readonly bool IsForCurrentRuleset;
|
||||||
|
|
||||||
|
private readonly int originalTargetColumns;
|
||||||
|
|
||||||
// Internal for testing purposes
|
// Internal for testing purposes
|
||||||
internal FastRandom Random { get; private set; }
|
internal FastRandom Random { get; private set; }
|
||||||
|
|
||||||
@ -65,6 +67,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
|||||||
else
|
else
|
||||||
TargetColumns = Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
|
TargetColumns = Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
originalTargetColumns = TargetColumns;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
|
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
|
||||||
@ -81,7 +85,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
|||||||
|
|
||||||
protected override Beatmap<ManiaHitObject> CreateBeatmap()
|
protected override Beatmap<ManiaHitObject> CreateBeatmap()
|
||||||
{
|
{
|
||||||
beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns });
|
beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns }, originalTargetColumns);
|
||||||
|
|
||||||
if (Dual)
|
if (Dual)
|
||||||
beatmap.Stages.Add(new StageDefinition { Columns = TargetColumns });
|
beatmap.Stages.Add(new StageDefinition { Columns = TargetColumns });
|
||||||
@ -116,7 +120,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
|||||||
prevNoteTimes.RemoveAt(0);
|
prevNoteTimes.RemoveAt(0);
|
||||||
prevNoteTimes.Add(newNoteTime);
|
prevNoteTimes.Add(newNoteTime);
|
||||||
|
|
||||||
density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;
|
if (prevNoteTimes.Count >= 2)
|
||||||
|
density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double lastTime;
|
private double lastTime;
|
||||||
@ -180,7 +185,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
|||||||
|
|
||||||
case IHasDuration endTimeData:
|
case IHasDuration endTimeData:
|
||||||
{
|
{
|
||||||
conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, originalBeatmap);
|
conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
|
||||||
|
|
||||||
recordNote(endTimeData.EndTime, new Vector2(256, 192));
|
recordNote(endTimeData.EndTime, new Vector2(256, 192));
|
||||||
computeDensity(endTimeData.EndTime);
|
computeDensity(endTimeData.EndTime);
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.MathUtils;
|
using osu.Game.Rulesets.Mania.MathUtils;
|
||||||
@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects;
|
|||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Beatmaps.Formats;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||||
{
|
{
|
||||||
@ -25,8 +26,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private const float osu_base_scoring_distance = 100;
|
private const float osu_base_scoring_distance = 100;
|
||||||
|
|
||||||
public readonly double EndTime;
|
public readonly int StartTime;
|
||||||
public readonly double SegmentDuration;
|
public readonly int EndTime;
|
||||||
|
public readonly int SegmentDuration;
|
||||||
public readonly int SpanCount;
|
public readonly int SpanCount;
|
||||||
|
|
||||||
private PatternType convertType;
|
private PatternType convertType;
|
||||||
@ -41,20 +43,26 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
var distanceData = hitObject as IHasDistance;
|
var distanceData = hitObject as IHasDistance;
|
||||||
var repeatsData = hitObject as IHasRepeats;
|
var repeatsData = hitObject as IHasRepeats;
|
||||||
|
|
||||||
SpanCount = repeatsData?.SpanCount() ?? 1;
|
Debug.Assert(distanceData != null);
|
||||||
|
|
||||||
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
|
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
|
||||||
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
|
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
|
||||||
|
|
||||||
// The true distance, accounting for any repeats
|
double beatLength;
|
||||||
double distance = (distanceData?.Distance ?? 0) * SpanCount;
|
#pragma warning disable 618
|
||||||
// The velocity of the osu! hit object - calculated as the velocity of a slider
|
if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint)
|
||||||
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength;
|
#pragma warning restore 618
|
||||||
// The duration of the osu! hit object
|
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
|
||||||
double osuDuration = distance / osuVelocity;
|
else
|
||||||
|
beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
|
||||||
|
|
||||||
EndTime = hitObject.StartTime + osuDuration;
|
SpanCount = repeatsData?.SpanCount() ?? 1;
|
||||||
SegmentDuration = (EndTime - HitObject.StartTime) / SpanCount;
|
StartTime = (int)Math.Round(hitObject.StartTime);
|
||||||
|
|
||||||
|
// This matches stable's calculation.
|
||||||
|
EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier);
|
||||||
|
|
||||||
|
SegmentDuration = (EndTime - StartTime) / SpanCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<Pattern> Generate()
|
public override IEnumerable<Pattern> Generate()
|
||||||
@ -76,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
|
|
||||||
foreach (var obj in originalPattern.HitObjects)
|
foreach (var obj in originalPattern.HitObjects)
|
||||||
{
|
{
|
||||||
if (!Precision.AlmostEquals(EndTime, obj.GetEndTime()))
|
if (EndTime != (int)Math.Round(obj.GetEndTime()))
|
||||||
intermediatePattern.Add(obj);
|
intermediatePattern.Add(obj);
|
||||||
else
|
else
|
||||||
endTimePattern.Add(obj);
|
endTimePattern.Add(obj);
|
||||||
@ -91,35 +99,35 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
if (TotalColumns == 1)
|
if (TotalColumns == 1)
|
||||||
{
|
{
|
||||||
var pattern = new Pattern();
|
var pattern = new Pattern();
|
||||||
addToPattern(pattern, 0, HitObject.StartTime, EndTime);
|
addToPattern(pattern, 0, StartTime, EndTime);
|
||||||
return pattern;
|
return pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SpanCount > 1)
|
if (SpanCount > 1)
|
||||||
{
|
{
|
||||||
if (SegmentDuration <= 90)
|
if (SegmentDuration <= 90)
|
||||||
return generateRandomHoldNotes(HitObject.StartTime, 1);
|
return generateRandomHoldNotes(StartTime, 1);
|
||||||
|
|
||||||
if (SegmentDuration <= 120)
|
if (SegmentDuration <= 120)
|
||||||
{
|
{
|
||||||
convertType |= PatternType.ForceNotStack;
|
convertType |= PatternType.ForceNotStack;
|
||||||
return generateRandomNotes(HitObject.StartTime, SpanCount + 1);
|
return generateRandomNotes(StartTime, SpanCount + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SegmentDuration <= 160)
|
if (SegmentDuration <= 160)
|
||||||
return generateStair(HitObject.StartTime);
|
return generateStair(StartTime);
|
||||||
|
|
||||||
if (SegmentDuration <= 200 && ConversionDifficulty > 3)
|
if (SegmentDuration <= 200 && ConversionDifficulty > 3)
|
||||||
return generateRandomMultipleNotes(HitObject.StartTime);
|
return generateRandomMultipleNotes(StartTime);
|
||||||
|
|
||||||
double duration = EndTime - HitObject.StartTime;
|
double duration = EndTime - StartTime;
|
||||||
if (duration >= 4000)
|
if (duration >= 4000)
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.23, 0, 0);
|
return generateNRandomNotes(StartTime, 0.23, 0, 0);
|
||||||
|
|
||||||
if (SegmentDuration > 400 && SpanCount < TotalColumns - 1 - RandomStart)
|
if (SegmentDuration > 400 && SpanCount < TotalColumns - 1 - RandomStart)
|
||||||
return generateTiledHoldNotes(HitObject.StartTime);
|
return generateTiledHoldNotes(StartTime);
|
||||||
|
|
||||||
return generateHoldAndNormalNotes(HitObject.StartTime);
|
return generateHoldAndNormalNotes(StartTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SegmentDuration <= 110)
|
if (SegmentDuration <= 110)
|
||||||
@ -128,37 +136,37 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
convertType |= PatternType.ForceNotStack;
|
convertType |= PatternType.ForceNotStack;
|
||||||
else
|
else
|
||||||
convertType &= ~PatternType.ForceNotStack;
|
convertType &= ~PatternType.ForceNotStack;
|
||||||
return generateRandomNotes(HitObject.StartTime, SegmentDuration < 80 ? 1 : 2);
|
return generateRandomNotes(StartTime, SegmentDuration < 80 ? 1 : 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ConversionDifficulty > 6.5)
|
if (ConversionDifficulty > 6.5)
|
||||||
{
|
{
|
||||||
if (convertType.HasFlag(PatternType.LowProbability))
|
if (convertType.HasFlag(PatternType.LowProbability))
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.78, 0.3, 0);
|
return generateNRandomNotes(StartTime, 0.78, 0.3, 0);
|
||||||
|
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.85, 0.36, 0.03);
|
return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ConversionDifficulty > 4)
|
if (ConversionDifficulty > 4)
|
||||||
{
|
{
|
||||||
if (convertType.HasFlag(PatternType.LowProbability))
|
if (convertType.HasFlag(PatternType.LowProbability))
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.43, 0.08, 0);
|
return generateNRandomNotes(StartTime, 0.43, 0.08, 0);
|
||||||
|
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.56, 0.18, 0);
|
return generateNRandomNotes(StartTime, 0.56, 0.18, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ConversionDifficulty > 2.5)
|
if (ConversionDifficulty > 2.5)
|
||||||
{
|
{
|
||||||
if (convertType.HasFlag(PatternType.LowProbability))
|
if (convertType.HasFlag(PatternType.LowProbability))
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.3, 0, 0);
|
return generateNRandomNotes(StartTime, 0.3, 0, 0);
|
||||||
|
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.37, 0.08, 0);
|
return generateNRandomNotes(StartTime, 0.37, 0.08, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (convertType.HasFlag(PatternType.LowProbability))
|
if (convertType.HasFlag(PatternType.LowProbability))
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.17, 0, 0);
|
return generateNRandomNotes(StartTime, 0.17, 0, 0);
|
||||||
|
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.27, 0, 0);
|
return generateNRandomNotes(StartTime, 0.27, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -167,7 +175,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// <param name="startTime">Start time of each hold note.</param>
|
/// <param name="startTime">Start time of each hold note.</param>
|
||||||
/// <param name="noteCount">Number of hold notes.</param>
|
/// <param name="noteCount">Number of hold notes.</param>
|
||||||
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
||||||
private Pattern generateRandomHoldNotes(double startTime, int noteCount)
|
private Pattern generateRandomHoldNotes(int startTime, int noteCount)
|
||||||
{
|
{
|
||||||
// - - - -
|
// - - - -
|
||||||
// ■ - ■ ■
|
// ■ - ■ ■
|
||||||
@ -202,7 +210,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// <param name="startTime">The start time.</param>
|
/// <param name="startTime">The start time.</param>
|
||||||
/// <param name="noteCount">The number of notes.</param>
|
/// <param name="noteCount">The number of notes.</param>
|
||||||
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
||||||
private Pattern generateRandomNotes(double startTime, int noteCount)
|
private Pattern generateRandomNotes(int startTime, int noteCount)
|
||||||
{
|
{
|
||||||
// - - - -
|
// - - - -
|
||||||
// x - - -
|
// x - - -
|
||||||
@ -234,7 +242,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="startTime">The start time.</param>
|
/// <param name="startTime">The start time.</param>
|
||||||
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
||||||
private Pattern generateStair(double startTime)
|
private Pattern generateStair(int startTime)
|
||||||
{
|
{
|
||||||
// - - - -
|
// - - - -
|
||||||
// x - - -
|
// x - - -
|
||||||
@ -286,7 +294,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="startTime">The start time.</param>
|
/// <param name="startTime">The start time.</param>
|
||||||
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
||||||
private Pattern generateRandomMultipleNotes(double startTime)
|
private Pattern generateRandomMultipleNotes(int startTime)
|
||||||
{
|
{
|
||||||
// - - - -
|
// - - - -
|
||||||
// x - - -
|
// x - - -
|
||||||
@ -329,7 +337,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// <param name="p3">The probability required for 3 hold notes to be generated.</param>
|
/// <param name="p3">The probability required for 3 hold notes to be generated.</param>
|
||||||
/// <param name="p4">The probability required for 4 hold notes to be generated.</param>
|
/// <param name="p4">The probability required for 4 hold notes to be generated.</param>
|
||||||
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
||||||
private Pattern generateNRandomNotes(double startTime, double p2, double p3, double p4)
|
private Pattern generateNRandomNotes(int startTime, double p2, double p3, double p4)
|
||||||
{
|
{
|
||||||
// - - - -
|
// - - - -
|
||||||
// ■ - ■ ■
|
// ■ - ■ ■
|
||||||
@ -366,7 +374,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH;
|
static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH;
|
||||||
|
|
||||||
bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability);
|
bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability);
|
||||||
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(HitObject.StartTime).Any(isDoubleSample);
|
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample);
|
||||||
|
|
||||||
if (canGenerateTwoNotes)
|
if (canGenerateTwoNotes)
|
||||||
p2 = 1;
|
p2 = 1;
|
||||||
@ -379,7 +387,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="startTime">The first hold note start time.</param>
|
/// <param name="startTime">The first hold note start time.</param>
|
||||||
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
||||||
private Pattern generateTiledHoldNotes(double startTime)
|
private Pattern generateTiledHoldNotes(int startTime)
|
||||||
{
|
{
|
||||||
// - - - -
|
// - - - -
|
||||||
// ■ ■ ■ ■
|
// ■ ■ ■ ■
|
||||||
@ -394,6 +402,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
|
|
||||||
int columnRepeat = Math.Min(SpanCount, TotalColumns);
|
int columnRepeat = Math.Min(SpanCount, TotalColumns);
|
||||||
|
|
||||||
|
// Due to integer rounding, this is not guaranteed to be the same as EndTime (the class-level variable).
|
||||||
|
int endTime = startTime + SegmentDuration * SpanCount;
|
||||||
|
|
||||||
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
|
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
|
||||||
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
|
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
|
||||||
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
|
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
|
||||||
@ -401,7 +412,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
for (int i = 0; i < columnRepeat; i++)
|
for (int i = 0; i < columnRepeat; i++)
|
||||||
{
|
{
|
||||||
nextColumn = FindAvailableColumn(nextColumn, pattern);
|
nextColumn = FindAvailableColumn(nextColumn, pattern);
|
||||||
addToPattern(pattern, nextColumn, startTime, EndTime);
|
addToPattern(pattern, nextColumn, startTime, endTime);
|
||||||
startTime += SegmentDuration;
|
startTime += SegmentDuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -413,7 +424,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="startTime">The start time of notes.</param>
|
/// <param name="startTime">The start time of notes.</param>
|
||||||
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
||||||
private Pattern generateHoldAndNormalNotes(double startTime)
|
private Pattern generateHoldAndNormalNotes(int startTime)
|
||||||
{
|
{
|
||||||
// - - - -
|
// - - - -
|
||||||
// ■ x x -
|
// ■ x x -
|
||||||
@ -448,7 +459,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
|
|
||||||
for (int i = 0; i <= SpanCount; i++)
|
for (int i = 0; i <= SpanCount; i++)
|
||||||
{
|
{
|
||||||
if (!(ignoreHead && startTime == HitObject.StartTime))
|
if (!(ignoreHead && startTime == StartTime))
|
||||||
{
|
{
|
||||||
for (int j = 0; j < noteCount; j++)
|
for (int j = 0; j < noteCount; j++)
|
||||||
{
|
{
|
||||||
@ -471,19 +482,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time">The time to retrieve the sample info list from.</param>
|
/// <param name="time">The time to retrieve the sample info list from.</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private IList<HitSampleInfo> sampleInfoListAt(double time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
|
private IList<HitSampleInfo> sampleInfoListAt(int time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the list of node samples that occur at time greater than or equal to <paramref name="time"/>.
|
/// Retrieves the list of node samples that occur at time greater than or equal to <paramref name="time"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time">The time to retrieve node samples at.</param>
|
/// <param name="time">The time to retrieve node samples at.</param>
|
||||||
private List<IList<HitSampleInfo>> nodeSamplesAt(double time)
|
private List<IList<HitSampleInfo>> nodeSamplesAt(int time)
|
||||||
{
|
{
|
||||||
if (!(HitObject is IHasPathWithRepeats curveData))
|
if (!(HitObject is IHasPathWithRepeats curveData))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// mathematically speaking this should be a whole number always, but floating-point arithmetic is not so kind
|
var index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;
|
||||||
var index = (int)Math.Round(SegmentDuration == 0 ? 0 : (time - HitObject.StartTime) / SegmentDuration, MidpointRounding.AwayFromZero);
|
|
||||||
|
|
||||||
// avoid slicing the list & creating copies, if at all possible.
|
// avoid slicing the list & creating copies, if at all possible.
|
||||||
return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList();
|
return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList();
|
||||||
@ -496,7 +506,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// <param name="column">The column to add the note to.</param>
|
/// <param name="column">The column to add the note to.</param>
|
||||||
/// <param name="startTime">The start time of the note.</param>
|
/// <param name="startTime">The start time of the note.</param>
|
||||||
/// <param name="endTime">The end time of the note (set to <paramref name="startTime"/> for a non-hold note).</param>
|
/// <param name="endTime">The end time of the note (set to <paramref name="startTime"/> for a non-hold note).</param>
|
||||||
private void addToPattern(Pattern pattern, int column, double startTime, double endTime)
|
private void addToPattern(Pattern pattern, int column, int startTime, int endTime)
|
||||||
{
|
{
|
||||||
ManiaHitObject newObject;
|
ManiaHitObject newObject;
|
||||||
|
|
||||||
|
@ -14,12 +14,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
{
|
{
|
||||||
internal class EndTimeObjectPatternGenerator : PatternGenerator
|
internal class EndTimeObjectPatternGenerator : PatternGenerator
|
||||||
{
|
{
|
||||||
private readonly double endTime;
|
private readonly int endTime;
|
||||||
|
private readonly PatternType convertType;
|
||||||
|
|
||||||
public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, IBeatmap originalBeatmap)
|
public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
|
||||||
: base(random, hitObject, beatmap, new Pattern(), originalBeatmap)
|
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
|
||||||
{
|
{
|
||||||
endTime = (HitObject as IHasDuration)?.EndTime ?? 0;
|
endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0);
|
||||||
|
|
||||||
|
convertType = PreviousPattern.ColumnWithObjects == TotalColumns
|
||||||
|
? PatternType.None
|
||||||
|
: PatternType.ForceNotStack;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<Pattern> Generate()
|
public override IEnumerable<Pattern> Generate()
|
||||||
@ -40,18 +45,25 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 8:
|
case 8:
|
||||||
addToPattern(pattern, FindAvailableColumn(GetRandomColumn(), PreviousPattern), generateHold);
|
addToPattern(pattern, getRandomColumn(), generateHold);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if (TotalColumns > 0)
|
addToPattern(pattern, getRandomColumn(0), generateHold);
|
||||||
addToPattern(pattern, GetRandomColumn(), generateHold);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return pattern;
|
return pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int getRandomColumn(int? lowerBound = null)
|
||||||
|
{
|
||||||
|
if ((convertType & PatternType.ForceNotStack) > 0)
|
||||||
|
return FindAvailableColumn(GetRandomColumn(lowerBound), lowerBound, patterns: PreviousPattern);
|
||||||
|
|
||||||
|
return FindAvailableColumn(GetRandomColumn(lowerBound), lowerBound);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructs and adds a note to a pattern.
|
/// Constructs and adds a note to a pattern.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -397,7 +397,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
centreProbability = 0;
|
centreProbability = 0;
|
||||||
p2 = Math.Min(p2 * 2, 0.2);
|
|
||||||
|
// Stable requires rngValue > x, which is an inverse-probability. Lazer uses true probability (1 - x).
|
||||||
|
// But multiplying this value by 2 (stable) is not the same operation as dividing it by 2 (lazer),
|
||||||
|
// so it needs to be converted to from a probability and then back after the multiplication.
|
||||||
|
p2 = 1 - Math.Max((1 - p2) * 2, 0.8);
|
||||||
p3 = 0;
|
p3 = 0;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -408,11 +412,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
|
|
||||||
case 6:
|
case 6:
|
||||||
centreProbability = 0;
|
centreProbability = 0;
|
||||||
p2 = Math.Min(p2 * 2, 0.5);
|
|
||||||
p3 = Math.Min(p3 * 2, 0.15);
|
// Stable requires rngValue > x, which is an inverse-probability. Lazer uses true probability (1 - x).
|
||||||
|
// But multiplying this value by 2 (stable) is not the same operation as dividing it by 2 (lazer),
|
||||||
|
// so it needs to be converted to from a probability and then back after the multiplication.
|
||||||
|
p2 = 1 - Math.Max((1 - p2) * 2, 0.5);
|
||||||
|
p3 = 1 - Math.Max((1 - p3) * 2, 0.85);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The stable values were allowed to exceed 1, which indicate <0% probability.
|
||||||
|
// These values needs to be clamped otherwise GetRandomNoteCount() will throw an exception.
|
||||||
|
p2 = Math.Clamp(p2, 0, 1);
|
||||||
|
p3 = Math.Clamp(p3, 0, 1);
|
||||||
|
|
||||||
double centreVal = Random.NextDouble();
|
double centreVal = Random.NextDouble();
|
||||||
int noteCount = GetRandomNoteCount(p2, p3);
|
int noteCount = GetRandomNoteCount(p2, p3);
|
||||||
|
|
||||||
|
@ -8,5 +8,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
public class ManiaDifficultyAttributes : DifficultyAttributes
|
public class ManiaDifficultyAttributes : DifficultyAttributes
|
||||||
{
|
{
|
||||||
public double GreatHitWindow;
|
public double GreatHitWindow;
|
||||||
|
public double ScoreMultiplier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -10,10 +11,12 @@ using osu.Game.Rulesets.Difficulty.Skills;
|
|||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Mania.Difficulty.Skills;
|
using osu.Game.Rulesets.Mania.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Mania.MathUtils;
|
||||||
using osu.Game.Rulesets.Mania.Mods;
|
using osu.Game.Rulesets.Mania.Mods;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osu.Game.Rulesets.Mania.Scoring;
|
using osu.Game.Rulesets.Mania.Scoring;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Difficulty
|
namespace osu.Game.Rulesets.Mania.Difficulty
|
||||||
@ -23,11 +26,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
private const double star_scaling_factor = 0.018;
|
private const double star_scaling_factor = 0.018;
|
||||||
|
|
||||||
private readonly bool isForCurrentRuleset;
|
private readonly bool isForCurrentRuleset;
|
||||||
|
private readonly double originalOverallDifficulty;
|
||||||
|
|
||||||
public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
|
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
|
||||||
|
originalOverallDifficulty = beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
@ -40,64 +45,33 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
|
|
||||||
return new ManiaDifficultyAttributes
|
return new ManiaDifficultyAttributes
|
||||||
{
|
{
|
||||||
StarRating = difficultyValue(skills) * star_scaling_factor,
|
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
||||||
GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate,
|
GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate),
|
||||||
|
ScoreMultiplier = getScoreMultiplier(beatmap, mods),
|
||||||
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
|
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
|
||||||
Skills = skills
|
Skills = skills
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private double difficultyValue(Skill[] skills)
|
|
||||||
{
|
|
||||||
// Preprocess the strains to find the maximum overall + individual (aggregate) strain from each section
|
|
||||||
var overall = skills.OfType<Overall>().Single();
|
|
||||||
var aggregatePeaks = new List<double>(Enumerable.Repeat(0.0, overall.StrainPeaks.Count));
|
|
||||||
|
|
||||||
foreach (var individual in skills.OfType<Individual>())
|
|
||||||
{
|
|
||||||
for (int i = 0; i < individual.StrainPeaks.Count; i++)
|
|
||||||
{
|
|
||||||
double aggregate = individual.StrainPeaks[i] + overall.StrainPeaks[i];
|
|
||||||
|
|
||||||
if (aggregate > aggregatePeaks[i])
|
|
||||||
aggregatePeaks[i] = aggregate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
aggregatePeaks.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
|
|
||||||
|
|
||||||
double difficulty = 0;
|
|
||||||
double weight = 1;
|
|
||||||
|
|
||||||
// Difficulty is the weighted sum of the highest strains from every section.
|
|
||||||
foreach (double strain in aggregatePeaks)
|
|
||||||
{
|
|
||||||
difficulty += strain * weight;
|
|
||||||
weight *= 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
return difficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
{
|
{
|
||||||
for (int i = 1; i < beatmap.HitObjects.Count; i++)
|
var sortedObjects = beatmap.HitObjects.ToArray();
|
||||||
yield return new ManiaDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate);
|
|
||||||
|
LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
|
||||||
|
|
||||||
|
for (int i = 1; i < sortedObjects.Length; i++)
|
||||||
|
yield return new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap)
|
// Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
|
||||||
|
protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input;
|
||||||
|
|
||||||
|
protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
|
||||||
{
|
{
|
||||||
int columnCount = ((ManiaBeatmap)beatmap).TotalColumns;
|
new Strain(((ManiaBeatmap)beatmap).TotalColumns)
|
||||||
|
};
|
||||||
var skills = new List<Skill> { new Overall(columnCount) };
|
|
||||||
|
|
||||||
for (int i = 0; i < columnCount; i++)
|
|
||||||
skills.Add(new Individual(i, columnCount));
|
|
||||||
|
|
||||||
return skills.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Mod[] DifficultyAdjustmentMods
|
protected override Mod[] DifficultyAdjustmentMods
|
||||||
{
|
{
|
||||||
@ -122,12 +96,73 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
new ManiaModKey3(),
|
new ManiaModKey3(),
|
||||||
new ManiaModKey4(),
|
new ManiaModKey4(),
|
||||||
new ManiaModKey5(),
|
new ManiaModKey5(),
|
||||||
|
new MultiMod(new ManiaModKey5(), new ManiaModDualStages()),
|
||||||
new ManiaModKey6(),
|
new ManiaModKey6(),
|
||||||
|
new MultiMod(new ManiaModKey6(), new ManiaModDualStages()),
|
||||||
new ManiaModKey7(),
|
new ManiaModKey7(),
|
||||||
|
new MultiMod(new ManiaModKey7(), new ManiaModDualStages()),
|
||||||
new ManiaModKey8(),
|
new ManiaModKey8(),
|
||||||
|
new MultiMod(new ManiaModKey8(), new ManiaModDualStages()),
|
||||||
new ManiaModKey9(),
|
new ManiaModKey9(),
|
||||||
|
new MultiMod(new ManiaModKey9(), new ManiaModDualStages()),
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int getHitWindow300(Mod[] mods)
|
||||||
|
{
|
||||||
|
if (isForCurrentRuleset)
|
||||||
|
{
|
||||||
|
double od = Math.Min(10.0, Math.Max(0, 10.0 - originalOverallDifficulty));
|
||||||
|
return applyModAdjustments(34 + 3 * od, mods);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.Round(originalOverallDifficulty) > 4)
|
||||||
|
return applyModAdjustments(34, mods);
|
||||||
|
|
||||||
|
return applyModAdjustments(47, mods);
|
||||||
|
|
||||||
|
static int applyModAdjustments(double value, Mod[] mods)
|
||||||
|
{
|
||||||
|
if (mods.Any(m => m is ManiaModHardRock))
|
||||||
|
value /= 1.4;
|
||||||
|
else if (mods.Any(m => m is ManiaModEasy))
|
||||||
|
value *= 1.4;
|
||||||
|
|
||||||
|
if (mods.Any(m => m is ManiaModDoubleTime))
|
||||||
|
value *= 1.5;
|
||||||
|
else if (mods.Any(m => m is ManiaModHalfTime))
|
||||||
|
value *= 0.75;
|
||||||
|
|
||||||
|
return (int)value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double getScoreMultiplier(IBeatmap beatmap, Mod[] mods)
|
||||||
|
{
|
||||||
|
double scoreMultiplier = 1;
|
||||||
|
|
||||||
|
foreach (var m in mods)
|
||||||
|
{
|
||||||
|
switch (m)
|
||||||
|
{
|
||||||
|
case ManiaModNoFail _:
|
||||||
|
case ManiaModEasy _:
|
||||||
|
case ManiaModHalfTime _:
|
||||||
|
scoreMultiplier *= 0.5;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var maniaBeatmap = (ManiaBeatmap)beatmap;
|
||||||
|
int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns;
|
||||||
|
|
||||||
|
if (diff > 0)
|
||||||
|
scoreMultiplier *= 0.9;
|
||||||
|
else if (diff < 0)
|
||||||
|
scoreMultiplier *= 0.9 + 0.04 * diff;
|
||||||
|
|
||||||
|
return scoreMultiplier;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@ -29,8 +28,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
private int countMeh;
|
private int countMeh;
|
||||||
private int countMiss;
|
private int countMiss;
|
||||||
|
|
||||||
public ManiaPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score)
|
public ManiaPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
|
||||||
: base(ruleset, beatmap, score)
|
: base(ruleset, attributes, score)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
|
||||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Objects;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
|
||||||
{
|
|
||||||
public class Individual : Skill
|
|
||||||
{
|
|
||||||
protected override double SkillMultiplier => 1;
|
|
||||||
protected override double StrainDecayBase => 0.125;
|
|
||||||
|
|
||||||
private readonly double[] holdEndTimes;
|
|
||||||
|
|
||||||
private readonly int column;
|
|
||||||
|
|
||||||
public Individual(int column, int columnCount)
|
|
||||||
{
|
|
||||||
this.column = column;
|
|
||||||
|
|
||||||
holdEndTimes = new double[columnCount];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override double StrainValueOf(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
|
||||||
var endTime = maniaCurrent.BaseObject.GetEndTime();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (maniaCurrent.BaseObject.Column != column)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
// We give a slight bonus if something is held meanwhile
|
|
||||||
return holdEndTimes.Any(t => t > endTime) ? 2.5 : 2;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
|
||||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Objects;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
|
||||||
{
|
|
||||||
public class Overall : Skill
|
|
||||||
{
|
|
||||||
protected override double SkillMultiplier => 1;
|
|
||||||
protected override double StrainDecayBase => 0.3;
|
|
||||||
|
|
||||||
private readonly double[] holdEndTimes;
|
|
||||||
|
|
||||||
private readonly int columnCount;
|
|
||||||
|
|
||||||
public Overall(int columnCount)
|
|
||||||
{
|
|
||||||
this.columnCount = columnCount;
|
|
||||||
|
|
||||||
holdEndTimes = new double[columnCount];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override double StrainValueOf(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
|
||||||
var endTime = maniaCurrent.BaseObject.GetEndTime();
|
|
||||||
|
|
||||||
double holdFactor = 1.0; // Factor 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 < columnCount; i++)
|
|
||||||
{
|
|
||||||
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
|
|
||||||
if (current.BaseObject.StartTime < holdEndTimes[i] && endTime > holdEndTimes[i])
|
|
||||||
holdAddition = 1.0;
|
|
||||||
|
|
||||||
// ... this addition only is valid if there is _no_ other note with the same ending.
|
|
||||||
// Releasing multiple notes at the same time is just as easy as releasing one
|
|
||||||
if (endTime == holdEndTimes[i])
|
|
||||||
holdAddition = 0;
|
|
||||||
|
|
||||||
// We give a slight bonus if something is held meanwhile
|
|
||||||
if (holdEndTimes[i] > endTime)
|
|
||||||
holdFactor = 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
|
|
||||||
|
|
||||||
return (1 + holdAddition) * holdFactor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
80
osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||||
|
{
|
||||||
|
public class Strain : Skill
|
||||||
|
{
|
||||||
|
private const double individual_decay_base = 0.125;
|
||||||
|
private const double overall_decay_base = 0.30;
|
||||||
|
|
||||||
|
protected override double SkillMultiplier => 1;
|
||||||
|
protected override double StrainDecayBase => 1;
|
||||||
|
|
||||||
|
private readonly double[] holdEndTimes;
|
||||||
|
private readonly double[] individualStrains;
|
||||||
|
|
||||||
|
private double individualStrain;
|
||||||
|
private double overallStrain;
|
||||||
|
|
||||||
|
public Strain(int totalColumns)
|
||||||
|
{
|
||||||
|
holdEndTimes = new double[totalColumns];
|
||||||
|
individualStrains = new double[totalColumns];
|
||||||
|
overallStrain = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||||
|
var endTime = maniaCurrent.BaseObject.GetEndTime();
|
||||||
|
var column = maniaCurrent.BaseObject.Column;
|
||||||
|
|
||||||
|
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
|
||||||
|
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
|
||||||
|
|
||||||
|
// Fill up the holdEndTimes array
|
||||||
|
for (int i = 0; i < holdEndTimes.Length; ++i)
|
||||||
|
{
|
||||||
|
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
|
||||||
|
if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.BaseObject.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1))
|
||||||
|
holdAddition = 1.0;
|
||||||
|
|
||||||
|
// ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1
|
||||||
|
if (Precision.AlmostEquals(endTime, holdEndTimes[i], 1))
|
||||||
|
holdAddition = 0;
|
||||||
|
|
||||||
|
// We give a slight bonus to everything if something is held meanwhile
|
||||||
|
if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1))
|
||||||
|
holdFactor = 1.25;
|
||||||
|
|
||||||
|
// Decay individual strains
|
||||||
|
individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base);
|
||||||
|
}
|
||||||
|
|
||||||
|
holdEndTimes[column] = endTime;
|
||||||
|
|
||||||
|
// Increase individual strain in own column
|
||||||
|
individualStrains[column] += 2.0 * holdFactor;
|
||||||
|
individualStrain = individualStrains[column];
|
||||||
|
|
||||||
|
overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base) + (1 + holdAddition) * holdFactor;
|
||||||
|
|
||||||
|
return individualStrain + overallStrain - CurrentStrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override double GetPeakStrain(double offset)
|
||||||
|
=> applyDecay(individualStrain, offset - Previous[0].BaseObject.StartTime, individual_decay_base)
|
||||||
|
+ applyDecay(overallStrain, offset - Previous[0].BaseObject.StartTime, overall_decay_base);
|
||||||
|
|
||||||
|
private double applyDecay(double value, double deltaTime, double decayBase)
|
||||||
|
=> value * Math.Pow(decayBase, deltaTime / 1000);
|
||||||
|
}
|
||||||
|
}
|
@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
int minColumn = int.MaxValue;
|
int minColumn = int.MaxValue;
|
||||||
int maxColumn = int.MinValue;
|
int maxColumn = int.MinValue;
|
||||||
|
|
||||||
foreach (var obj in SelectedHitObjects.OfType<ManiaHitObject>())
|
foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>())
|
||||||
{
|
{
|
||||||
if (obj.Column < minColumn)
|
if (obj.Column < minColumn)
|
||||||
minColumn = obj.Column;
|
minColumn = obj.Column;
|
||||||
@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
|
|
||||||
columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn);
|
columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn);
|
||||||
|
|
||||||
foreach (var obj in SelectedHitObjects.OfType<ManiaHitObject>())
|
foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>())
|
||||||
obj.Column += columnDelta;
|
obj.Column += columnDelta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
|
|
||||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
|
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
|
||||||
|
|
||||||
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new ManiaPerformanceCalculator(this, beatmap, score);
|
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new ManiaPerformanceCalculator(this, attributes, score);
|
||||||
|
|
||||||
public const string SHORT_NAME = "mania";
|
public const string SHORT_NAME = "mania";
|
||||||
|
|
||||||
@ -319,6 +319,31 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast<int>().OrderByDescending(i => i).First(v => variant >= v);
|
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast<int>().OrderByDescending(i => i).First(v => variant >= v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<HitResult> GetValidHitResults()
|
||||||
|
{
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
HitResult.Perfect,
|
||||||
|
HitResult.Great,
|
||||||
|
HitResult.Good,
|
||||||
|
HitResult.Ok,
|
||||||
|
HitResult.Meh,
|
||||||
|
|
||||||
|
HitResult.LargeTickHit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string GetDisplayNameForHitResult(HitResult result)
|
||||||
|
{
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case HitResult.LargeTickHit:
|
||||||
|
return "hold tick";
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.GetDisplayNameForHitResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
|
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
|
||||||
{
|
{
|
||||||
new StatisticRow
|
new StatisticRow
|
||||||
|
@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
new SettingsEnumDropdown<ManiaScrollingDirection>
|
new SettingsEnumDropdown<ManiaScrollingDirection>
|
||||||
{
|
{
|
||||||
LabelText = "Scrolling direction",
|
LabelText = "Scrolling direction",
|
||||||
Bindable = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
|
Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
|
||||||
},
|
},
|
||||||
new SettingsSlider<double, TimeSlider>
|
new SettingsSlider<double, TimeSlider>
|
||||||
{
|
{
|
||||||
LabelText = "Scroll speed",
|
LabelText = "Scroll speed",
|
||||||
Bindable = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
|
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
|
||||||
KeyboardStep = 5
|
KeyboardStep = 5
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
165
osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.MathUtils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides access to .NET4.0 unstable sorting methods.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Source: https://referencesource.microsoft.com/#mscorlib/system/collections/generic/arraysorthelper.cs
|
||||||
|
/// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
/// </remarks>
|
||||||
|
internal static class LegacySortHelper<T>
|
||||||
|
{
|
||||||
|
private const int quick_sort_depth_threshold = 32;
|
||||||
|
|
||||||
|
public static void Sort(T[] keys, IComparer<T> comparer)
|
||||||
|
{
|
||||||
|
if (keys == null)
|
||||||
|
throw new ArgumentNullException(nameof(keys));
|
||||||
|
|
||||||
|
if (keys.Length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
comparer ??= Comparer<T>.Default;
|
||||||
|
depthLimitedQuickSort(keys, 0, keys.Length - 1, comparer, quick_sort_depth_threshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void depthLimitedQuickSort(T[] keys, int left, int right, IComparer<T> comparer, int depthLimit)
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (depthLimit == 0)
|
||||||
|
{
|
||||||
|
heapsort(keys, left, right, comparer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int i = left;
|
||||||
|
int j = right;
|
||||||
|
|
||||||
|
// pre-sort the low, middle (pivot), and high values in place.
|
||||||
|
// this improves performance in the face of already sorted data, or
|
||||||
|
// data that is made up of multiple sorted runs appended together.
|
||||||
|
int middle = i + ((j - i) >> 1);
|
||||||
|
swapIfGreater(keys, comparer, i, middle); // swap the low with the mid point
|
||||||
|
swapIfGreater(keys, comparer, i, j); // swap the low with the high
|
||||||
|
swapIfGreater(keys, comparer, middle, j); // swap the middle with the high
|
||||||
|
|
||||||
|
T x = keys[middle];
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
while (comparer.Compare(keys[i], x) < 0) i++;
|
||||||
|
while (comparer.Compare(x, keys[j]) < 0) j--;
|
||||||
|
Contract.Assert(i >= left && j <= right, "(i>=left && j<=right) Sort failed - Is your IComparer bogus?");
|
||||||
|
if (i > j) break;
|
||||||
|
|
||||||
|
if (i < j)
|
||||||
|
{
|
||||||
|
T key = keys[i];
|
||||||
|
keys[i] = keys[j];
|
||||||
|
keys[j] = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
j--;
|
||||||
|
} while (i <= j);
|
||||||
|
|
||||||
|
// The next iteration of the while loop is to "recursively" sort the larger half of the array and the
|
||||||
|
// following calls recrusively sort the smaller half. So we subtrack one from depthLimit here so
|
||||||
|
// both sorts see the new value.
|
||||||
|
depthLimit--;
|
||||||
|
|
||||||
|
if (j - left <= right - i)
|
||||||
|
{
|
||||||
|
if (left < j) depthLimitedQuickSort(keys, left, j, comparer, depthLimit);
|
||||||
|
left = i;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (i < right) depthLimitedQuickSort(keys, i, right, comparer, depthLimit);
|
||||||
|
right = j;
|
||||||
|
}
|
||||||
|
} while (left < right);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void heapsort(T[] keys, int lo, int hi, IComparer<T> comparer)
|
||||||
|
{
|
||||||
|
Contract.Requires(keys != null);
|
||||||
|
Contract.Requires(comparer != null);
|
||||||
|
Contract.Requires(lo >= 0);
|
||||||
|
Contract.Requires(hi > lo);
|
||||||
|
Contract.Requires(hi < keys.Length);
|
||||||
|
|
||||||
|
int n = hi - lo + 1;
|
||||||
|
|
||||||
|
for (int i = n / 2; i >= 1; i = i - 1)
|
||||||
|
{
|
||||||
|
downHeap(keys, i, n, lo, comparer);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = n; i > 1; i = i - 1)
|
||||||
|
{
|
||||||
|
swap(keys, lo, lo + i - 1);
|
||||||
|
downHeap(keys, 1, i - 1, lo, comparer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void downHeap(T[] keys, int i, int n, int lo, IComparer<T> comparer)
|
||||||
|
{
|
||||||
|
Contract.Requires(keys != null);
|
||||||
|
Contract.Requires(comparer != null);
|
||||||
|
Contract.Requires(lo >= 0);
|
||||||
|
Contract.Requires(lo < keys.Length);
|
||||||
|
|
||||||
|
T d = keys[lo + i - 1];
|
||||||
|
|
||||||
|
while (i <= n / 2)
|
||||||
|
{
|
||||||
|
var child = 2 * i;
|
||||||
|
|
||||||
|
if (child < n && comparer.Compare(keys[lo + child - 1], keys[lo + child]) < 0)
|
||||||
|
{
|
||||||
|
child++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(comparer.Compare(d, keys[lo + child - 1]) < 0))
|
||||||
|
break;
|
||||||
|
|
||||||
|
keys[lo + i - 1] = keys[lo + child - 1];
|
||||||
|
i = child;
|
||||||
|
}
|
||||||
|
|
||||||
|
keys[lo + i - 1] = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void swap(T[] a, int i, int j)
|
||||||
|
{
|
||||||
|
if (i != j)
|
||||||
|
{
|
||||||
|
T t = a[i];
|
||||||
|
a[i] = a[j];
|
||||||
|
a[j] = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void swapIfGreater(T[] keys, IComparer<T> comparer, int a, int b)
|
||||||
|
{
|
||||||
|
if (a != b)
|
||||||
|
{
|
||||||
|
if (comparer.Compare(keys[a], keys[b]) > 0)
|
||||||
|
{
|
||||||
|
T key = keys[a];
|
||||||
|
keys[a] = keys[b];
|
||||||
|
keys[b] = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
typeof(ManiaModKey7),
|
typeof(ManiaModKey7),
|
||||||
typeof(ManiaModKey8),
|
typeof(ManiaModKey8),
|
||||||
typeof(ManiaModKey9),
|
typeof(ManiaModKey9),
|
||||||
|
typeof(ManiaModKey10),
|
||||||
}.Except(new[] { GetType() }).ToArray();
|
}.Except(new[] { GetType() }).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
["soft-hitnormal"],
|
["soft-hitnormal"],
|
||||||
["drum-hitnormal"]
|
["drum-hitnormal"]
|
||||||
],
|
],
|
||||||
"Samples": ["drum-hitnormal"]
|
"Samples": ["-hitnormal"]
|
||||||
}, {
|
}, {
|
||||||
"StartTime": 1875.0,
|
"StartTime": 1875.0,
|
||||||
"EndTime": 2750.0,
|
"EndTime": 2750.0,
|
||||||
@ -19,7 +19,7 @@
|
|||||||
["soft-hitnormal"],
|
["soft-hitnormal"],
|
||||||
["drum-hitnormal"]
|
["drum-hitnormal"]
|
||||||
],
|
],
|
||||||
"Samples": ["drum-hitnormal"]
|
"Samples": ["-hitnormal"]
|
||||||
}]
|
}]
|
||||||
}, {
|
}, {
|
||||||
"StartTime": 3750.0,
|
"StartTime": 3750.0,
|
||||||
|
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 45 KiB |
41
osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// 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.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
|
{
|
||||||
|
public class TestPlayfieldBorder : OsuTestScene
|
||||||
|
{
|
||||||
|
public TestPlayfieldBorder()
|
||||||
|
{
|
||||||
|
Bindable<PlayfieldBorderStyle> playfieldBorderStyle = new Bindable<PlayfieldBorderStyle>();
|
||||||
|
|
||||||
|
AddStep("add drawables", () =>
|
||||||
|
{
|
||||||
|
Child = new Container
|
||||||
|
{
|
||||||
|
Size = new Vector2(400, 300),
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new PlayfieldBorder
|
||||||
|
{
|
||||||
|
PlayfieldBorderStyle = { BindTarget = playfieldBorderStyle }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("Set none", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.None);
|
||||||
|
AddStep("Set corners", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.Corners);
|
||||||
|
AddStep("Set full", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.Full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Configuration;
|
using osu.Game.Rulesets.Configuration;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Configuration
|
namespace osu.Game.Rulesets.Osu.Configuration
|
||||||
{
|
{
|
||||||
@ -19,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
|
|||||||
Set(OsuRulesetSetting.SnakingInSliders, true);
|
Set(OsuRulesetSetting.SnakingInSliders, true);
|
||||||
Set(OsuRulesetSetting.SnakingOutSliders, true);
|
Set(OsuRulesetSetting.SnakingOutSliders, true);
|
||||||
Set(OsuRulesetSetting.ShowCursorTrail, true);
|
Set(OsuRulesetSetting.ShowCursorTrail, true);
|
||||||
|
Set(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
|
|||||||
{
|
{
|
||||||
SnakingInSliders,
|
SnakingInSliders,
|
||||||
SnakingOutSliders,
|
SnakingOutSliders,
|
||||||
ShowCursorTrail
|
ShowCursorTrail,
|
||||||
|
PlayfieldBorderStyle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
public double SpeedStrain;
|
public double SpeedStrain;
|
||||||
public double ApproachRate;
|
public double ApproachRate;
|
||||||
public double OverallDifficulty;
|
public double OverallDifficulty;
|
||||||
|
public int HitCircleCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
|
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
|
||||||
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
|
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
|
||||||
|
|
||||||
|
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
||||||
|
|
||||||
return new OsuDifficultyAttributes
|
return new OsuDifficultyAttributes
|
||||||
{
|
{
|
||||||
StarRating = starRating,
|
StarRating = starRating,
|
||||||
@ -56,6 +58,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||||
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
||||||
MaxCombo = maxCombo,
|
MaxCombo = maxCombo,
|
||||||
|
HitCircleCount = hitCirclesCount,
|
||||||
Skills = skills
|
Skills = skills
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,9 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
|
||||||
@ -19,9 +17,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
{
|
{
|
||||||
public new OsuDifficultyAttributes Attributes => (OsuDifficultyAttributes)base.Attributes;
|
public new OsuDifficultyAttributes Attributes => (OsuDifficultyAttributes)base.Attributes;
|
||||||
|
|
||||||
private readonly int countHitCircles;
|
|
||||||
private readonly int beatmapMaxCombo;
|
|
||||||
|
|
||||||
private Mod[] mods;
|
private Mod[] mods;
|
||||||
|
|
||||||
private double accuracy;
|
private double accuracy;
|
||||||
@ -31,14 +26,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
private int countMeh;
|
private int countMeh;
|
||||||
private int countMiss;
|
private int countMiss;
|
||||||
|
|
||||||
public OsuPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score)
|
public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
|
||||||
: base(ruleset, beatmap, score)
|
: base(ruleset, attributes, score)
|
||||||
{
|
{
|
||||||
countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle);
|
|
||||||
|
|
||||||
beatmapMaxCombo = Beatmap.HitObjects.Count;
|
|
||||||
// Add the ticks + tail of the slider. 1 is subtracted because the "headcircle" would be counted twice (once for the slider itself in the line above)
|
|
||||||
beatmapMaxCombo += Beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override double Calculate(Dictionary<string, double> categoryRatings = null)
|
public override double Calculate(Dictionary<string, double> categoryRatings = null)
|
||||||
@ -81,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
categoryRatings.Add("Accuracy", accuracyValue);
|
categoryRatings.Add("Accuracy", accuracyValue);
|
||||||
categoryRatings.Add("OD", Attributes.OverallDifficulty);
|
categoryRatings.Add("OD", Attributes.OverallDifficulty);
|
||||||
categoryRatings.Add("AR", Attributes.ApproachRate);
|
categoryRatings.Add("AR", Attributes.ApproachRate);
|
||||||
categoryRatings.Add("Max Combo", beatmapMaxCombo);
|
categoryRatings.Add("Max Combo", Attributes.MaxCombo);
|
||||||
}
|
}
|
||||||
|
|
||||||
return totalValue;
|
return totalValue;
|
||||||
@ -106,8 +96,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
aimValue *= Math.Pow(0.97, countMiss);
|
aimValue *= Math.Pow(0.97, countMiss);
|
||||||
|
|
||||||
// Combo scaling
|
// Combo scaling
|
||||||
if (beatmapMaxCombo > 0)
|
if (Attributes.MaxCombo > 0)
|
||||||
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0);
|
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
|
||||||
|
|
||||||
double approachRateFactor = 1.0;
|
double approachRateFactor = 1.0;
|
||||||
|
|
||||||
@ -154,8 +144,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
speedValue *= Math.Pow(0.97, countMiss);
|
speedValue *= Math.Pow(0.97, countMiss);
|
||||||
|
|
||||||
// Combo scaling
|
// Combo scaling
|
||||||
if (beatmapMaxCombo > 0)
|
if (Attributes.MaxCombo > 0)
|
||||||
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0);
|
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
|
||||||
|
|
||||||
double approachRateFactor = 1.0;
|
double approachRateFactor = 1.0;
|
||||||
if (Attributes.ApproachRate > 10.33)
|
if (Attributes.ApproachRate > 10.33)
|
||||||
@ -178,7 +168,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
{
|
{
|
||||||
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window
|
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window
|
||||||
double betterAccuracyPercentage;
|
double betterAccuracyPercentage;
|
||||||
int amountHitObjectsWithAccuracy = countHitCircles;
|
int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;
|
||||||
|
|
||||||
if (amountHitObjectsWithAccuracy > 0)
|
if (amountHitObjectsWithAccuracy > 0)
|
||||||
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
|
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
|
||||||
|
@ -21,6 +21,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
|||||||
InternalChild = circlePiece = new HitCirclePiece();
|
InternalChild = circlePiece = new HitCirclePiece();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
BeginPlacement();
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
@ -49,6 +49,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
OriginPosition = body.PathOffset;
|
OriginPosition = body.PathOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RecyclePath() => body.RecyclePath();
|
||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos);
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,10 +24,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
{
|
{
|
||||||
public class SliderSelectionBlueprint : OsuSelectionBlueprint<Slider>
|
public class SliderSelectionBlueprint : OsuSelectionBlueprint<Slider>
|
||||||
{
|
{
|
||||||
protected readonly SliderBodyPiece BodyPiece;
|
protected SliderBodyPiece BodyPiece { get; private set; }
|
||||||
protected readonly SliderCircleSelectionBlueprint HeadBlueprint;
|
protected SliderCircleSelectionBlueprint HeadBlueprint { get; private set; }
|
||||||
protected readonly SliderCircleSelectionBlueprint TailBlueprint;
|
protected SliderCircleSelectionBlueprint TailBlueprint { get; private set; }
|
||||||
protected readonly PathControlPointVisualiser ControlPointVisualiser;
|
protected PathControlPointVisualiser ControlPointVisualiser { get; private set; }
|
||||||
|
|
||||||
|
private readonly DrawableSlider slider;
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private HitObjectComposer composer { get; set; }
|
private HitObjectComposer composer { get; set; }
|
||||||
@ -44,17 +46,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
public SliderSelectionBlueprint(DrawableSlider slider)
|
public SliderSelectionBlueprint(DrawableSlider slider)
|
||||||
: base(slider)
|
: base(slider)
|
||||||
{
|
{
|
||||||
var sliderObject = (Slider)slider.HitObject;
|
this.slider = slider;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
BodyPiece = new SliderBodyPiece(),
|
BodyPiece = new SliderBodyPiece(),
|
||||||
HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
|
HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
|
||||||
TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End),
|
TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End),
|
||||||
ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true)
|
|
||||||
{
|
|
||||||
RemoveControlPointsRequested = removeControlPoints
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,13 +68,35 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
|
|
||||||
pathVersion = HitObject.Path.Version.GetBoundCopy();
|
pathVersion = HitObject.Path.Version.GetBoundCopy();
|
||||||
pathVersion.BindValueChanged(_ => updatePath());
|
pathVersion.BindValueChanged(_ => updatePath());
|
||||||
|
|
||||||
|
BodyPiece.UpdateFrom(HitObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
BodyPiece.UpdateFrom(HitObject);
|
if (IsSelected)
|
||||||
|
BodyPiece.UpdateFrom(HitObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnSelected()
|
||||||
|
{
|
||||||
|
AddInternal(ControlPointVisualiser = new PathControlPointVisualiser((Slider)slider.HitObject, true)
|
||||||
|
{
|
||||||
|
RemoveControlPointsRequested = removeControlPoints
|
||||||
|
});
|
||||||
|
|
||||||
|
base.OnSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDeselected()
|
||||||
|
{
|
||||||
|
base.OnDeselected();
|
||||||
|
|
||||||
|
// throw away frame buffers on deselection.
|
||||||
|
ControlPointVisualiser?.Expire();
|
||||||
|
BodyPiece.RecyclePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector2 rightClickPosition;
|
private Vector2 rightClickPosition;
|
||||||
@ -182,7 +206,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
private void updatePath()
|
private void updatePath()
|
||||||
{
|
{
|
||||||
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
||||||
editorBeatmap?.UpdateHitObject(HitObject);
|
editorBeatmap?.Update(HitObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override MenuItem[] ContextMenuItems => new MenuItem[]
|
public override MenuItem[] ContextMenuItems => new MenuItem[]
|
||||||
|
@ -56,7 +56,18 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both });
|
LayerBelowRuleset.AddRange(new Drawable[]
|
||||||
|
{
|
||||||
|
new PlayfieldBorder
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
|
||||||
|
},
|
||||||
|
distanceSnapGridContainer = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
|
selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
|
||||||
selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
|
selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -10,40 +17,268 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
public class OsuSelectionHandler : SelectionHandler
|
public class OsuSelectionHandler : SelectionHandler
|
||||||
{
|
{
|
||||||
public override bool HandleMovement(MoveSelectionEvent moveEvent)
|
protected override void OnSelectionChanged()
|
||||||
{
|
{
|
||||||
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
|
base.OnSelectionChanged();
|
||||||
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
|
|
||||||
|
|
||||||
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
|
bool canOperate = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
|
||||||
foreach (var h in SelectedHitObjects.OfType<OsuHitObject>())
|
|
||||||
|
SelectionBox.CanRotate = canOperate;
|
||||||
|
SelectionBox.CanScaleX = canOperate;
|
||||||
|
SelectionBox.CanScaleY = canOperate;
|
||||||
|
SelectionBox.CanReverse = canOperate;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnOperationEnded()
|
||||||
|
{
|
||||||
|
base.OnOperationEnded();
|
||||||
|
referenceOrigin = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool HandleMovement(MoveSelectionEvent moveEvent) =>
|
||||||
|
moveSelection(moveEvent.InstantDelta);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// During a transform, the initial origin is stored so it can be used throughout the operation.
|
||||||
|
/// </summary>
|
||||||
|
private Vector2? referenceOrigin;
|
||||||
|
|
||||||
|
public override bool HandleReverse()
|
||||||
|
{
|
||||||
|
var hitObjects = selectedMovableObjects;
|
||||||
|
|
||||||
|
double endTime = hitObjects.Max(h => h.GetEndTime());
|
||||||
|
double startTime = hitObjects.Min(h => h.StartTime);
|
||||||
|
|
||||||
|
bool moreThanOneObject = hitObjects.Length > 1;
|
||||||
|
|
||||||
|
foreach (var h in hitObjects)
|
||||||
{
|
{
|
||||||
if (h is Spinner)
|
if (moreThanOneObject)
|
||||||
|
h.StartTime = endTime - (h.GetEndTime() - startTime);
|
||||||
|
|
||||||
|
if (h is Slider slider)
|
||||||
{
|
{
|
||||||
// Spinners don't support position adjustments
|
var points = slider.Path.ControlPoints.ToArray();
|
||||||
continue;
|
Vector2 endPos = points.Last().Position.Value;
|
||||||
|
|
||||||
|
slider.Path.ControlPoints.Clear();
|
||||||
|
|
||||||
|
slider.Position += endPos;
|
||||||
|
|
||||||
|
PathType? lastType = null;
|
||||||
|
|
||||||
|
for (var i = 0; i < points.Length; i++)
|
||||||
|
{
|
||||||
|
var p = points[i];
|
||||||
|
p.Position.Value -= endPos;
|
||||||
|
|
||||||
|
// propagate types forwards to last null type
|
||||||
|
if (i == points.Length - 1)
|
||||||
|
p.Type.Value = lastType;
|
||||||
|
else if (p.Type.Value != null)
|
||||||
|
{
|
||||||
|
var newType = p.Type.Value;
|
||||||
|
p.Type.Value = lastType;
|
||||||
|
lastType = newType;
|
||||||
|
}
|
||||||
|
|
||||||
|
slider.Path.ControlPoints.Insert(0, p);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stacking is not considered
|
|
||||||
minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
|
|
||||||
maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minPosition.X < 0 || minPosition.Y < 0 || maxPosition.X > DrawWidth || maxPosition.Y > DrawHeight)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
foreach (var h in SelectedHitObjects.OfType<OsuHitObject>())
|
|
||||||
{
|
|
||||||
if (h is Spinner)
|
|
||||||
{
|
|
||||||
// Spinners don't support position adjustments
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Position += moveEvent.InstantDelta;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool HandleFlip(Direction direction)
|
||||||
|
{
|
||||||
|
var hitObjects = selectedMovableObjects;
|
||||||
|
|
||||||
|
var selectedObjectsQuad = getSurroundingQuad(hitObjects);
|
||||||
|
var centre = selectedObjectsQuad.Centre;
|
||||||
|
|
||||||
|
foreach (var h in hitObjects)
|
||||||
|
{
|
||||||
|
var pos = h.Position;
|
||||||
|
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case Direction.Horizontal:
|
||||||
|
pos.X = centre.X - (pos.X - centre.X);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Direction.Vertical:
|
||||||
|
pos.Y = centre.Y - (pos.Y - centre.Y);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Position = pos;
|
||||||
|
|
||||||
|
if (h is Slider slider)
|
||||||
|
{
|
||||||
|
foreach (var point in slider.Path.ControlPoints)
|
||||||
|
{
|
||||||
|
point.Position.Value = new Vector2(
|
||||||
|
(direction == Direction.Horizontal ? -1 : 1) * point.Position.Value.X,
|
||||||
|
(direction == Direction.Vertical ? -1 : 1) * point.Position.Value.Y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool HandleScale(Vector2 scale, Anchor reference)
|
||||||
|
{
|
||||||
|
adjustScaleFromAnchor(ref scale, reference);
|
||||||
|
|
||||||
|
var hitObjects = selectedMovableObjects;
|
||||||
|
|
||||||
|
// for the time being, allow resizing of slider paths only if the slider is
|
||||||
|
// the only hit object selected. with a group selection, it's likely the user
|
||||||
|
// is not looking to change the duration of the slider but expand the whole pattern.
|
||||||
|
if (hitObjects.Length == 1 && hitObjects.First() is Slider slider)
|
||||||
|
{
|
||||||
|
Quad quad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
|
||||||
|
Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / quad.Width, 1 + scale.Y / quad.Height);
|
||||||
|
|
||||||
|
foreach (var point in slider.Path.ControlPoints)
|
||||||
|
point.Position.Value *= pathRelativeDeltaScale;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// move the selection before scaling if dragging from top or left anchors.
|
||||||
|
if ((reference & Anchor.x0) > 0 && !moveSelection(new Vector2(-scale.X, 0))) return false;
|
||||||
|
if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false;
|
||||||
|
|
||||||
|
Quad quad = getSurroundingQuad(hitObjects);
|
||||||
|
|
||||||
|
foreach (var h in hitObjects)
|
||||||
|
{
|
||||||
|
h.Position = new Vector2(
|
||||||
|
quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X),
|
||||||
|
quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
|
||||||
|
{
|
||||||
|
// cancel out scale in axes we don't care about (based on which drag handle was used).
|
||||||
|
if ((reference & Anchor.x1) > 0) scale.X = 0;
|
||||||
|
if ((reference & Anchor.y1) > 0) scale.Y = 0;
|
||||||
|
|
||||||
|
// reverse the scale direction if dragging from top or left.
|
||||||
|
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
|
||||||
|
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool HandleRotation(float delta)
|
||||||
|
{
|
||||||
|
var hitObjects = selectedMovableObjects;
|
||||||
|
|
||||||
|
Quad quad = getSurroundingQuad(hitObjects);
|
||||||
|
|
||||||
|
referenceOrigin ??= quad.Centre;
|
||||||
|
|
||||||
|
foreach (var h in hitObjects)
|
||||||
|
{
|
||||||
|
h.Position = rotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
|
||||||
|
|
||||||
|
if (h is IHasPath path)
|
||||||
|
{
|
||||||
|
foreach (var point in path.Path.ControlPoints)
|
||||||
|
point.Position.Value = rotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this isn't always the case but let's be lenient for now.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool moveSelection(Vector2 delta)
|
||||||
|
{
|
||||||
|
var hitObjects = selectedMovableObjects;
|
||||||
|
|
||||||
|
Quad quad = getSurroundingQuad(hitObjects);
|
||||||
|
|
||||||
|
if (quad.TopLeft.X + delta.X < 0 ||
|
||||||
|
quad.TopLeft.Y + delta.Y < 0 ||
|
||||||
|
quad.BottomRight.X + delta.X > DrawWidth ||
|
||||||
|
quad.BottomRight.Y + delta.Y > DrawHeight)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
foreach (var h in hitObjects)
|
||||||
|
h.Position += delta;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a gamefield-space quad surrounding the provided hit objects.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hitObjects">The hit objects to calculate a quad for.</param>
|
||||||
|
private Quad getSurroundingQuad(OsuHitObject[] hitObjects) =>
|
||||||
|
getSurroundingQuad(hitObjects.SelectMany(h => new[] { h.Position, h.EndPosition }));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a gamefield-space quad surrounding the provided points.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="points">The points to calculate a quad for.</param>
|
||||||
|
private Quad getSurroundingQuad(IEnumerable<Vector2> points)
|
||||||
|
{
|
||||||
|
if (!EditorBeatmap.SelectedHitObjects.Any())
|
||||||
|
return new Quad();
|
||||||
|
|
||||||
|
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
|
||||||
|
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
|
||||||
|
|
||||||
|
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
|
||||||
|
foreach (var p in points)
|
||||||
|
{
|
||||||
|
minPosition = Vector2.ComponentMin(minPosition, p);
|
||||||
|
maxPosition = Vector2.ComponentMax(maxPosition, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 size = maxPosition - minPosition;
|
||||||
|
|
||||||
|
return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All osu! hitobjects which can be moved/rotated/scaled.
|
||||||
|
/// </summary>
|
||||||
|
private OsuHitObject[] selectedMovableObjects => EditorBeatmap.SelectedHitObjects
|
||||||
|
.OfType<OsuHitObject>()
|
||||||
|
.Where(h => !(h is Spinner))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rotate a point around an arbitrary origin.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="point">The point.</param>
|
||||||
|
/// <param name="origin">The centre origin to rotate around.</param>
|
||||||
|
/// <param name="angle">The angle to rotate (in degrees).</param>
|
||||||
|
private static Vector2 rotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle)
|
||||||
|
{
|
||||||
|
angle = -angle;
|
||||||
|
|
||||||
|
point.X -= origin.X;
|
||||||
|
point.Y -= origin.Y;
|
||||||
|
|
||||||
|
Vector2 ret;
|
||||||
|
ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
|
||||||
|
ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
|
||||||
|
|
||||||
|
ret.X += origin.X;
|
||||||
|
ret.Y += origin.Y;
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@ using osu.Game.Rulesets.Mods;
|
|||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Replays;
|
using osu.Game.Rulesets.Osu.Replays;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Screens.Play;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Mods
|
namespace osu.Game.Rulesets.Osu.Mods
|
||||||
{
|
{
|
||||||
@ -31,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
private OsuInputManager inputManager;
|
private OsuInputManager inputManager;
|
||||||
|
|
||||||
private GameplayClock gameplayClock;
|
private IFrameStableClock gameplayClock;
|
||||||
|
|
||||||
private List<OsuReplayFrame> replayFrames;
|
private List<OsuReplayFrame> replayFrames;
|
||||||
|
|
||||||
|
@ -39,7 +39,14 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
base.ApplyToDrawableHitObjects(drawables);
|
base.ApplyToDrawableHitObjects(drawables);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state)
|
private double lastSliderHeadFadeOutStartTime;
|
||||||
|
private double lastSliderHeadFadeOutDuration;
|
||||||
|
|
||||||
|
protected override void ApplyFirstObjectIncreaseVisibilityState(DrawableHitObject drawable, ArmedState state) => applyState(drawable, true);
|
||||||
|
|
||||||
|
protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state) => applyState(drawable, false);
|
||||||
|
|
||||||
|
private void applyState(DrawableHitObject drawable, bool increaseVisibility)
|
||||||
{
|
{
|
||||||
if (!(drawable is DrawableOsuHitObject d))
|
if (!(drawable is DrawableOsuHitObject d))
|
||||||
return;
|
return;
|
||||||
@ -54,15 +61,52 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
switch (drawable)
|
switch (drawable)
|
||||||
{
|
{
|
||||||
|
case DrawableSliderTail sliderTail:
|
||||||
|
// use stored values from head circle to achieve same fade sequence.
|
||||||
|
fadeOutDuration = lastSliderHeadFadeOutDuration;
|
||||||
|
fadeOutStartTime = lastSliderHeadFadeOutStartTime;
|
||||||
|
|
||||||
|
using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
|
||||||
|
sliderTail.FadeOut(fadeOutDuration);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DrawableSliderRepeat sliderRepeat:
|
||||||
|
// use stored values from head circle to achieve same fade sequence.
|
||||||
|
fadeOutDuration = lastSliderHeadFadeOutDuration;
|
||||||
|
fadeOutStartTime = lastSliderHeadFadeOutStartTime;
|
||||||
|
|
||||||
|
using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
|
||||||
|
// only apply to circle piece – reverse arrow is not affected by hidden.
|
||||||
|
sliderRepeat.CirclePiece.FadeOut(fadeOutDuration);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
case DrawableHitCircle circle:
|
case DrawableHitCircle circle:
|
||||||
// we don't want to see the approach circle
|
|
||||||
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
|
if (circle is DrawableSliderHead)
|
||||||
circle.ApproachCircle.Hide();
|
{
|
||||||
|
lastSliderHeadFadeOutDuration = fadeOutDuration;
|
||||||
|
lastSliderHeadFadeOutStartTime = fadeOutStartTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
Drawable fadeTarget = circle;
|
||||||
|
|
||||||
|
if (increaseVisibility)
|
||||||
|
{
|
||||||
|
// only fade the circle piece (not the approach circle) for the increased visibility object.
|
||||||
|
fadeTarget = circle.CirclePiece;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// we don't want to see the approach circle
|
||||||
|
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
|
||||||
|
circle.ApproachCircle.Hide();
|
||||||
|
}
|
||||||
|
|
||||||
// fade out immediately after fade in.
|
// fade out immediately after fade in.
|
||||||
using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
|
using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
|
||||||
circle.FadeOut(fadeOutDuration);
|
fadeTarget.FadeOut(fadeOutDuration);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DrawableSlider slider:
|
case DrawableSlider slider:
|
||||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
@ -38,20 +39,25 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
protected void ApplyTraceableState(DrawableHitObject drawable, ArmedState state)
|
protected void ApplyTraceableState(DrawableHitObject drawable, ArmedState state)
|
||||||
{
|
{
|
||||||
if (!(drawable is DrawableOsuHitObject drawableOsu))
|
if (!(drawable is DrawableOsuHitObject))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var h = drawableOsu.HitObject;
|
|
||||||
|
|
||||||
//todo: expose and hide spinner background somehow
|
//todo: expose and hide spinner background somehow
|
||||||
|
|
||||||
switch (drawable)
|
switch (drawable)
|
||||||
{
|
{
|
||||||
case DrawableHitCircle circle:
|
case DrawableHitCircle circle:
|
||||||
// we only want to see the approach circle
|
// we only want to see the approach circle
|
||||||
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
|
applyCirclePieceState(circle, circle.CirclePiece);
|
||||||
circle.CirclePiece.Hide();
|
break;
|
||||||
|
|
||||||
|
case DrawableSliderTail sliderTail:
|
||||||
|
applyCirclePieceState(sliderTail);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DrawableSliderRepeat sliderRepeat:
|
||||||
|
// show only the repeat arrow
|
||||||
|
applyCirclePieceState(sliderRepeat, sliderRepeat.CirclePiece);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DrawableSlider slider:
|
case DrawableSlider slider:
|
||||||
@ -61,6 +67,13 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyCirclePieceState(DrawableOsuHitObject hitObject, IDrawable hitCircle = null)
|
||||||
|
{
|
||||||
|
var h = hitObject.HitObject;
|
||||||
|
using (hitObject.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
|
||||||
|
(hitCircle ?? hitObject).Hide();
|
||||||
|
}
|
||||||
|
|
||||||
private void applySliderState(DrawableSlider slider)
|
private void applySliderState(DrawableSlider slider)
|
||||||
{
|
{
|
||||||
((PlaySliderBody)slider.Body.Drawable).AccentColour = slider.AccentColour.Value.Opacity(0);
|
((PlaySliderBody)slider.Body.Drawable).AccentColour = slider.AccentColour.Value.Opacity(0);
|
||||||
|
@ -51,6 +51,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
|
Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
|
||||||
|
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
|
||||||
tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
|
tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
|
||||||
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
|
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
|
||||||
Ball = new SliderBall(s, this)
|
Ball = new SliderBall(s, this)
|
||||||
@ -62,7 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
Alpha = 0
|
Alpha = 0
|
||||||
},
|
},
|
||||||
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
|
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
|
||||||
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,9 +6,11 @@ using System.Collections.Generic;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
||||||
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
@ -22,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
private readonly Drawable scaleContainer;
|
private readonly Drawable scaleContainer;
|
||||||
|
|
||||||
|
public readonly Drawable CirclePiece;
|
||||||
|
|
||||||
public override bool DisplayResult => false;
|
public override bool DisplayResult => false;
|
||||||
|
|
||||||
public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider)
|
public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider)
|
||||||
@ -34,7 +38,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
InternalChild = scaleContainer = new ReverseArrowPiece();
|
InternalChild = scaleContainer = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Children = new[]
|
||||||
|
{
|
||||||
|
// no default for this; only visible in legacy skins.
|
||||||
|
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()),
|
||||||
|
arrow = new ReverseArrowPiece(),
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly IBindable<float> scaleBindable = new BindableFloat();
|
private readonly IBindable<float> scaleBindable = new BindableFloat();
|
||||||
@ -85,6 +100,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
private bool hasRotation;
|
private bool hasRotation;
|
||||||
|
|
||||||
|
private readonly ReverseArrowPiece arrow;
|
||||||
|
|
||||||
public void UpdateSnakingPosition(Vector2 start, Vector2 end)
|
public void UpdateSnakingPosition(Vector2 start, Vector2 end)
|
||||||
{
|
{
|
||||||
// When the repeat is hit, the arrow should fade out on spot rather than following the slider
|
// When the repeat is hit, the arrow should fade out on spot rather than following the slider
|
||||||
@ -114,18 +131,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
}
|
}
|
||||||
|
|
||||||
float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
|
float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
|
||||||
while (Math.Abs(aimRotation - Rotation) > 180)
|
while (Math.Abs(aimRotation - arrow.Rotation) > 180)
|
||||||
aimRotation += aimRotation < Rotation ? 360 : -360;
|
aimRotation += aimRotation < arrow.Rotation ? 360 : -360;
|
||||||
|
|
||||||
if (!hasRotation)
|
if (!hasRotation)
|
||||||
{
|
{
|
||||||
Rotation = aimRotation;
|
arrow.Rotation = aimRotation;
|
||||||
hasRotation = true;
|
hasRotation = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// If we're already snaking, interpolate to smooth out sharp curves (linear sliders, mainly).
|
// If we're already snaking, interpolate to smooth out sharp curves (linear sliders, mainly).
|
||||||
Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Rotation, aimRotation, 0, 50, Easing.OutQuint);
|
arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
{
|
{
|
||||||
public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking
|
public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking
|
||||||
{
|
{
|
||||||
private readonly Slider slider;
|
private readonly SliderTailCircle tailCircle;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The judgement text is provided by the <see cref="DrawableSlider"/>.
|
/// The judgement text is provided by the <see cref="DrawableSlider"/>.
|
||||||
@ -18,28 +23,73 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
public bool Tracking { get; set; }
|
public bool Tracking { get; set; }
|
||||||
|
|
||||||
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
|
private readonly IBindable<float> scaleBindable = new BindableFloat();
|
||||||
private readonly IBindable<int> pathVersion = new Bindable<int>();
|
|
||||||
|
|
||||||
public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle)
|
private readonly SkinnableDrawable circlePiece;
|
||||||
: base(hitCircle)
|
|
||||||
|
private readonly Container scaleContainer;
|
||||||
|
|
||||||
|
public DrawableSliderTail(Slider slider, SliderTailCircle tailCircle)
|
||||||
|
: base(tailCircle)
|
||||||
{
|
{
|
||||||
this.slider = slider;
|
this.tailCircle = tailCircle;
|
||||||
|
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||||
FillMode = FillMode.Fit;
|
|
||||||
|
|
||||||
AlwaysPresent = true;
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
scaleContainer = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
// no default for this; only visible in legacy skins.
|
||||||
|
circlePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
positionBindable.BindTo(hitCircle.PositionBindable);
|
[BackgroundDependencyLoader]
|
||||||
pathVersion.BindTo(slider.Path.Version);
|
private void load()
|
||||||
|
{
|
||||||
|
scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true);
|
||||||
|
scaleBindable.BindTo(HitObject.ScaleBindable);
|
||||||
|
}
|
||||||
|
|
||||||
positionBindable.BindValueChanged(_ => updatePosition());
|
protected override void UpdateInitialTransforms()
|
||||||
pathVersion.BindValueChanged(_ => updatePosition(), true);
|
{
|
||||||
|
base.UpdateInitialTransforms();
|
||||||
|
|
||||||
// TODO: This has no drawable content. Support for skins should be added.
|
circlePiece.FadeInFromZero(HitObject.TimeFadeIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void UpdateStateTransforms(ArmedState state)
|
||||||
|
{
|
||||||
|
base.UpdateStateTransforms(state);
|
||||||
|
|
||||||
|
Debug.Assert(HitObject.HitWindows != null);
|
||||||
|
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case ArmedState.Idle:
|
||||||
|
this.Delay(HitObject.TimePreempt).FadeOut(500);
|
||||||
|
|
||||||
|
Expire(true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ArmedState.Miss:
|
||||||
|
this.FadeOut(100);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ArmedState.Hit:
|
||||||
|
// todo: temporary / arbitrary
|
||||||
|
this.Delay(800).FadeOut();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
@ -48,6 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updatePosition() => Position = HitObject.Position - slider.Position;
|
public void UpdateSnakingPosition(Vector2 start, Vector2 end) =>
|
||||||
|
Position = tailCircle.RepeatIndex % 2 == 0 ? end : start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// Trigger a miss result for remaining ticks to avoid infinite gameplay.
|
// Trigger a miss result for remaining ticks to avoid infinite gameplay.
|
||||||
foreach (var tick in ticks.Where(t => !t.IsHit))
|
foreach (var tick in ticks.Where(t => !t.Result.HasResult))
|
||||||
tick.TriggerResult(false);
|
tick.TriggerResult(false);
|
||||||
|
|
||||||
ApplyResult(r =>
|
ApplyResult(r =>
|
||||||
@ -268,7 +268,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
while (wholeSpins != spins)
|
while (wholeSpins != spins)
|
||||||
{
|
{
|
||||||
var tick = ticks.FirstOrDefault(t => !t.IsHit);
|
var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
|
||||||
|
|
||||||
// tick may be null if we've hit the spin limit.
|
// tick may be null if we've hit the spin limit.
|
||||||
if (tick != null)
|
if (tick != null)
|
||||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
|||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
Masking = true;
|
Masking = true;
|
||||||
BorderThickness = 10;
|
BorderThickness = 9; // roughly matches slider borders and makes stacked circles distinctly visible from each other.
|
||||||
BorderColour = Color4.White;
|
BorderColour = Color4.White;
|
||||||
|
|
||||||
Child = new Box
|
Child = new Box
|
||||||
|
@ -137,6 +137,10 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
|
|
||||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||||
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
|
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
|
||||||
|
|
||||||
|
// The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
|
||||||
|
// For now, the samples are attached to and played by the slider itself at the correct end time.
|
||||||
|
Samples = this.GetNodeSamples(repeatCount + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
@ -176,6 +180,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
// if this is to change, we should revisit this.
|
// if this is to change, we should revisit this.
|
||||||
AddNested(TailCircle = new SliderTailCircle(this)
|
AddNested(TailCircle = new SliderTailCircle(this)
|
||||||
{
|
{
|
||||||
|
RepeatIndex = e.SpanIndex,
|
||||||
StartTime = e.Time,
|
StartTime = e.Time,
|
||||||
Position = EndPosition,
|
Position = EndPosition,
|
||||||
StackHeight = StackHeight
|
StackHeight = StackHeight
|
||||||
@ -183,10 +188,9 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case SliderEventType.Repeat:
|
case SliderEventType.Repeat:
|
||||||
AddNested(new SliderRepeat
|
AddNested(new SliderRepeat(this)
|
||||||
{
|
{
|
||||||
RepeatIndex = e.SpanIndex,
|
RepeatIndex = e.SpanIndex,
|
||||||
SpanDuration = SpanDuration,
|
|
||||||
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
|
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
|
||||||
Position = Position + Path.PositionAt(e.PathProgress),
|
Position = Position + Path.PositionAt(e.PathProgress),
|
||||||
StackHeight = StackHeight,
|
StackHeight = StackHeight,
|
||||||
@ -230,15 +234,12 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
tick.Samples = sampleList;
|
tick.Samples = sampleList;
|
||||||
|
|
||||||
foreach (var repeat in NestedHitObjects.OfType<SliderRepeat>())
|
foreach (var repeat in NestedHitObjects.OfType<SliderRepeat>())
|
||||||
repeat.Samples = getNodeSamples(repeat.RepeatIndex + 1);
|
repeat.Samples = this.GetNodeSamples(repeat.RepeatIndex + 1);
|
||||||
|
|
||||||
if (HeadCircle != null)
|
if (HeadCircle != null)
|
||||||
HeadCircle.Samples = getNodeSamples(0);
|
HeadCircle.Samples = this.GetNodeSamples(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IList<HitSampleInfo> getNodeSamples(int nodeIndex) =>
|
|
||||||
nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
|
|
||||||
|
|
||||||
public override Judgement CreateJudgement() => new OsuIgnoreJudgement();
|
public override Judgement CreateJudgement() => new OsuIgnoreJudgement();
|
||||||
|
|
||||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||||
|
50
osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// 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.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A hit circle which is at the end of a slider path (either repeat or final tail).
|
||||||
|
/// </summary>
|
||||||
|
public abstract class SliderEndCircle : HitCircle
|
||||||
|
{
|
||||||
|
private readonly Slider slider;
|
||||||
|
|
||||||
|
protected SliderEndCircle(Slider slider)
|
||||||
|
{
|
||||||
|
this.slider = slider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int RepeatIndex { get; set; }
|
||||||
|
|
||||||
|
public double SpanDuration => slider.SpanDuration;
|
||||||
|
|
||||||
|
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
||||||
|
{
|
||||||
|
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||||
|
|
||||||
|
if (RepeatIndex > 0)
|
||||||
|
{
|
||||||
|
// Repeat points after the first span should appear behind the still-visible one.
|
||||||
|
TimeFadeIn = 0;
|
||||||
|
|
||||||
|
// The next end circle should appear exactly after the previous circle (on the same end) is hit.
|
||||||
|
TimePreempt = SpanDuration * 2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// taken from osu-stable
|
||||||
|
const float first_end_circle_preempt_adjust = 2 / 3f;
|
||||||
|
|
||||||
|
// The first end circle should fade in with the slider.
|
||||||
|
TimePreempt = (StartTime - slider.StartTime) + slider.TimePreempt * first_end_circle_preempt_adjust;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||||
|
}
|
||||||
|
}
|
@ -1,35 +1,19 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Osu.Judgements;
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects
|
namespace osu.Game.Rulesets.Osu.Objects
|
||||||
{
|
{
|
||||||
public class SliderRepeat : OsuHitObject
|
public class SliderRepeat : SliderEndCircle
|
||||||
{
|
{
|
||||||
public int RepeatIndex { get; set; }
|
public SliderRepeat(Slider slider)
|
||||||
public double SpanDuration { get; set; }
|
: base(slider)
|
||||||
|
|
||||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
|
||||||
{
|
{
|
||||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
|
||||||
|
|
||||||
// Out preempt should be one span early to give the user ample warning.
|
|
||||||
TimePreempt += SpanDuration;
|
|
||||||
|
|
||||||
// We want to show the first RepeatPoint as the TimePreempt dictates but on short (and possibly fast) sliders
|
|
||||||
// we may need to cut down this time on following RepeatPoints to only show up to two RepeatPoints at any given time.
|
|
||||||
if (RepeatIndex > 0)
|
|
||||||
TimePreempt = Math.Min(SpanDuration * 2, TimePreempt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
|
||||||
|
|
||||||
public override Judgement CreateJudgement() => new SliderRepeatJudgement();
|
public override Judgement CreateJudgement() => new SliderRepeatJudgement();
|
||||||
|
|
||||||
public class SliderRepeatJudgement : OsuJudgement
|
public class SliderRepeatJudgement : OsuJudgement
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Judgements;
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
@ -13,18 +12,13 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
/// Note that this should not be used for timing correctness.
|
/// Note that this should not be used for timing correctness.
|
||||||
/// See <see cref="SliderEventType.LegacyLastTick"/> usage in <see cref="Slider"/> for more information.
|
/// See <see cref="SliderEventType.LegacyLastTick"/> usage in <see cref="Slider"/> for more information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SliderTailCircle : SliderCircle
|
public class SliderTailCircle : SliderEndCircle
|
||||||
{
|
{
|
||||||
private readonly IBindable<int> pathVersion = new Bindable<int>();
|
|
||||||
|
|
||||||
public SliderTailCircle(Slider slider)
|
public SliderTailCircle(Slider slider)
|
||||||
|
: base(slider)
|
||||||
{
|
{
|
||||||
pathVersion.BindTo(slider.Path.Version);
|
|
||||||
pathVersion.BindValueChanged(_ => Position = slider.EndPosition);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
|
||||||
|
|
||||||
public override Judgement CreateJudgement() => new SliderTailJudgement();
|
public override Judgement CreateJudgement() => new SliderTailJudgement();
|
||||||
|
|
||||||
public class SliderTailJudgement : OsuJudgement
|
public class SliderTailJudgement : OsuJudgement
|
||||||
|
@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
|
|
||||||
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(this, beatmap);
|
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(this, beatmap);
|
||||||
|
|
||||||
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new OsuPerformanceCalculator(this, beatmap, score);
|
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new OsuPerformanceCalculator(this, attributes, score);
|
||||||
|
|
||||||
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);
|
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);
|
||||||
|
|
||||||
@ -191,6 +191,41 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
|
|
||||||
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
|
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
|
||||||
|
|
||||||
|
protected override IEnumerable<HitResult> GetValidHitResults()
|
||||||
|
{
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
HitResult.Great,
|
||||||
|
HitResult.Ok,
|
||||||
|
HitResult.Meh,
|
||||||
|
|
||||||
|
HitResult.LargeTickHit,
|
||||||
|
HitResult.SmallTickHit,
|
||||||
|
HitResult.SmallBonus,
|
||||||
|
HitResult.LargeBonus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string GetDisplayNameForHitResult(HitResult result)
|
||||||
|
{
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case HitResult.LargeTickHit:
|
||||||
|
return "slider tick";
|
||||||
|
|
||||||
|
case HitResult.SmallTickHit:
|
||||||
|
return "slider end";
|
||||||
|
|
||||||
|
case HitResult.SmallBonus:
|
||||||
|
return "spinner spin";
|
||||||
|
|
||||||
|
case HitResult.LargeBonus:
|
||||||
|
return "spinner bonus";
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.GetDisplayNameForHitResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
|
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
|
||||||
{
|
{
|
||||||
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
|
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
|
||||||
|
@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
ReverseArrow,
|
ReverseArrow,
|
||||||
HitCircleText,
|
HitCircleText,
|
||||||
SliderHeadHitCircle,
|
SliderHeadHitCircle,
|
||||||
|
SliderTailHitCircle,
|
||||||
SliderFollowCircle,
|
SliderFollowCircle,
|
||||||
SliderBall,
|
SliderBall,
|
||||||
SliderBody,
|
SliderBody,
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
@ -15,6 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
|
|
||||||
private bool disjointTrail;
|
private bool disjointTrail;
|
||||||
private double lastTrailTime;
|
private double lastTrailTime;
|
||||||
|
private IBindable<float> cursorSize;
|
||||||
|
|
||||||
public LegacyCursorTrail()
|
public LegacyCursorTrail()
|
||||||
{
|
{
|
||||||
@ -22,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(ISkinSource skin)
|
private void load(ISkinSource skin, OsuConfigManager config)
|
||||||
{
|
{
|
||||||
Texture = skin.GetTexture("cursortrail");
|
Texture = skin.GetTexture("cursortrail");
|
||||||
disjointTrail = skin.GetTexture("cursormiddle") == null;
|
disjointTrail = skin.GetTexture("cursormiddle") == null;
|
||||||
@ -32,12 +36,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
// stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation.
|
// stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation.
|
||||||
Texture.ScaleAdjust *= 1.6f;
|
Texture.ScaleAdjust *= 1.6f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cursorSize = config.GetBindable<float>(OsuSetting.GameplayCursorSize).GetBoundCopy();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override double FadeDuration => disjointTrail ? 150 : 500;
|
protected override double FadeDuration => disjointTrail ? 150 : 500;
|
||||||
|
|
||||||
protected override bool InterpolateMovements => !disjointTrail;
|
protected override bool InterpolateMovements => !disjointTrail;
|
||||||
|
|
||||||
|
protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
|
||||||
|
|
||||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||||
{
|
{
|
||||||
if (!disjointTrail)
|
if (!disjointTrail)
|
||||||
|
@ -21,10 +21,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
public class LegacyMainCirclePiece : CompositeDrawable
|
public class LegacyMainCirclePiece : CompositeDrawable
|
||||||
{
|
{
|
||||||
private readonly string priorityLookup;
|
private readonly string priorityLookup;
|
||||||
|
private readonly bool hasNumber;
|
||||||
|
|
||||||
public LegacyMainCirclePiece(string priorityLookup = null)
|
public LegacyMainCirclePiece(string priorityLookup = null, bool hasNumber = true)
|
||||||
{
|
{
|
||||||
this.priorityLookup = priorityLookup;
|
this.priorityLookup = priorityLookup;
|
||||||
|
this.hasNumber = hasNumber;
|
||||||
|
|
||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||||
}
|
}
|
||||||
@ -47,6 +49,23 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
{
|
{
|
||||||
OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject;
|
OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject;
|
||||||
|
|
||||||
|
bool allowFallback = false;
|
||||||
|
|
||||||
|
// attempt lookup using priority specification
|
||||||
|
Texture baseTexture = getTextureWithFallback(string.Empty);
|
||||||
|
|
||||||
|
// if the base texture was not found without a fallback, switch on fallback mode and re-perform the lookup.
|
||||||
|
if (baseTexture == null)
|
||||||
|
{
|
||||||
|
allowFallback = true;
|
||||||
|
baseTexture = getTextureWithFallback(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
// at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it.
|
||||||
|
// the flow above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
|
||||||
|
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin).
|
||||||
|
Texture overlayTexture = getTextureWithFallback("overlay");
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
circleSprites = new Container<Sprite>
|
circleSprites = new Container<Sprite>
|
||||||
@ -58,19 +77,23 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
{
|
{
|
||||||
hitCircleSprite = new Sprite
|
hitCircleSprite = new Sprite
|
||||||
{
|
{
|
||||||
Texture = getTextureWithFallback(string.Empty),
|
Texture = baseTexture,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
},
|
},
|
||||||
hitCircleOverlay = new Sprite
|
hitCircleOverlay = new Sprite
|
||||||
{
|
{
|
||||||
Texture = getTextureWithFallback("overlay"),
|
Texture = overlayTexture,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
|
};
|
||||||
|
|
||||||
|
if (hasNumber)
|
||||||
|
{
|
||||||
|
AddInternal(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
|
||||||
{
|
{
|
||||||
Font = OsuFont.Numeric.With(size: 40),
|
Font = OsuFont.Numeric.With(size: 40),
|
||||||
UseFullGlyphHeight = false,
|
UseFullGlyphHeight = false,
|
||||||
@ -78,8 +101,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
},
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
|
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
|
||||||
|
|
||||||
@ -95,8 +118,13 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
Texture tex = null;
|
Texture tex = null;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(priorityLookup))
|
if (!string.IsNullOrEmpty(priorityLookup))
|
||||||
|
{
|
||||||
tex = skin.GetTexture($"{priorityLookup}{name}");
|
tex = skin.GetTexture($"{priorityLookup}{name}");
|
||||||
|
|
||||||
|
if (!allowFallback)
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
return tex ?? skin.GetTexture($"hitcircle{name}");
|
return tex ?? skin.GetTexture($"hitcircle{name}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,7 +135,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
|
|
||||||
state.BindValueChanged(updateState, true);
|
state.BindValueChanged(updateState, true);
|
||||||
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
|
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
|
||||||
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
|
if (hasNumber)
|
||||||
|
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateState(ValueChangedEvent<ArmedState> state)
|
private void updateState(ValueChangedEvent<ArmedState> state)
|
||||||
@ -120,16 +149,19 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
|
circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
|
||||||
circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
||||||
|
|
||||||
var legacyVersion = skin.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value;
|
if (hasNumber)
|
||||||
|
|
||||||
if (legacyVersion >= 2.0m)
|
|
||||||
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
|
|
||||||
hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
// old skins scale and fade it normally along other pieces.
|
var legacyVersion = skin.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value;
|
||||||
hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
|
|
||||||
hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
if (legacyVersion >= 2.0m)
|
||||||
|
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
|
||||||
|
hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// old skins scale and fade it normally along other pieces.
|
||||||
|
hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
|
||||||
|
hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
@ -66,6 +66,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
case OsuSkinComponents.SliderTailHitCircle:
|
||||||
|
if (hasHitCircle.Value)
|
||||||
|
return new LegacyMainCirclePiece("sliderendcircle", false);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
case OsuSkinComponents.SliderHeadHitCircle:
|
case OsuSkinComponents.SliderHeadHitCircle:
|
||||||
if (hasHitCircle.Value)
|
if (hasHitCircle.Value)
|
||||||
return new LegacyMainCirclePiece("sliderstartcircle");
|
return new LegacyMainCirclePiece("sliderstartcircle");
|
||||||
|
@ -119,6 +119,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual bool InterpolateMovements => true;
|
protected virtual bool InterpolateMovements => true;
|
||||||
|
|
||||||
|
protected virtual float IntervalMultiplier => 1.0f;
|
||||||
|
|
||||||
private Vector2? lastPosition;
|
private Vector2? lastPosition;
|
||||||
private readonly InputResampler resampler = new InputResampler();
|
private readonly InputResampler resampler = new InputResampler();
|
||||||
|
|
||||||
@ -147,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
float distance = diff.Length;
|
float distance = diff.Length;
|
||||||
Vector2 direction = diff / distance;
|
Vector2 direction = diff / distance;
|
||||||
|
|
||||||
float interval = partSize.X / 2.5f;
|
float interval = partSize.X / 2.5f * IntervalMultiplier;
|
||||||
|
|
||||||
for (float d = interval; d < distance; d += interval)
|
for (float d = interval; d < distance; d += interval)
|
||||||
{
|
{
|
||||||
|
@ -17,12 +17,16 @@ using osu.Game.Rulesets.Osu.UI.Cursor;
|
|||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Rulesets.Osu.Configuration;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
{
|
{
|
||||||
public class OsuPlayfield : Playfield
|
public class OsuPlayfield : Playfield
|
||||||
{
|
{
|
||||||
|
private readonly PlayfieldBorder playfieldBorder;
|
||||||
private readonly ProxyContainer approachCircles;
|
private readonly ProxyContainer approachCircles;
|
||||||
private readonly ProxyContainer spinnerProxies;
|
private readonly ProxyContainer spinnerProxies;
|
||||||
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
|
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
|
||||||
@ -33,12 +37,19 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
|
|
||||||
protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer();
|
protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer();
|
||||||
|
|
||||||
|
private readonly Bindable<bool> playfieldBorderStyle = new BindableBool();
|
||||||
|
|
||||||
private readonly IDictionary<HitResult, DrawablePool<DrawableOsuJudgement>> poolDictionary = new Dictionary<HitResult, DrawablePool<DrawableOsuJudgement>>();
|
private readonly IDictionary<HitResult, DrawablePool<DrawableOsuJudgement>> poolDictionary = new Dictionary<HitResult, DrawablePool<DrawableOsuJudgement>>();
|
||||||
|
|
||||||
public OsuPlayfield()
|
public OsuPlayfield()
|
||||||
{
|
{
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
|
playfieldBorder = new PlayfieldBorder
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Depth = 3
|
||||||
|
},
|
||||||
spinnerProxies = new ProxyContainer
|
spinnerProxies = new ProxyContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
@ -76,6 +87,12 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
AddRangeInternal(poolDictionary.Values);
|
AddRangeInternal(poolDictionary.Values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader(true)]
|
||||||
|
private void load(OsuRulesetConfigManager config)
|
||||||
|
{
|
||||||
|
config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle);
|
||||||
|
}
|
||||||
|
|
||||||
public override void Add(DrawableHitObject h)
|
public override void Add(DrawableHitObject h)
|
||||||
{
|
{
|
||||||
h.OnNewResult += onNewResult;
|
h.OnNewResult += onNewResult;
|
||||||
|
@ -5,6 +5,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Overlays.Settings;
|
using osu.Game.Overlays.Settings;
|
||||||
using osu.Game.Rulesets.Osu.Configuration;
|
using osu.Game.Rulesets.Osu.Configuration;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
{
|
{
|
||||||
@ -27,17 +28,22 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
new SettingsCheckbox
|
new SettingsCheckbox
|
||||||
{
|
{
|
||||||
LabelText = "Snaking in sliders",
|
LabelText = "Snaking in sliders",
|
||||||
Bindable = config.GetBindable<bool>(OsuRulesetSetting.SnakingInSliders)
|
Current = config.GetBindable<bool>(OsuRulesetSetting.SnakingInSliders)
|
||||||
},
|
},
|
||||||
new SettingsCheckbox
|
new SettingsCheckbox
|
||||||
{
|
{
|
||||||
LabelText = "Snaking out sliders",
|
LabelText = "Snaking out sliders",
|
||||||
Bindable = config.GetBindable<bool>(OsuRulesetSetting.SnakingOutSliders)
|
Current = config.GetBindable<bool>(OsuRulesetSetting.SnakingOutSliders)
|
||||||
},
|
},
|
||||||
new SettingsCheckbox
|
new SettingsCheckbox
|
||||||
{
|
{
|
||||||
LabelText = "Cursor trail",
|
LabelText = "Cursor trail",
|
||||||
Bindable = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorTrail)
|
Current = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorTrail)
|
||||||
|
},
|
||||||
|
new SettingsEnumDropdown<PlayfieldBorderStyle>
|
||||||
|
{
|
||||||
|
LabelText = "Playfield border style",
|
||||||
|
Current = config.GetBindable<PlayfieldBorderStyle>(OsuRulesetSetting.PlayfieldBorderStyle),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
|||||||
converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x =>
|
converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x =>
|
||||||
{
|
{
|
||||||
TaikoHitObject first = x.First();
|
TaikoHitObject first = x.First();
|
||||||
if (x.Skip(1).Any() && !(first is Swell))
|
if (x.Skip(1).Any() && first.CanBeStrong)
|
||||||
first.IsStrong = true;
|
first.IsStrong = true;
|
||||||
return first;
|
return first;
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
@ -5,7 +5,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@ -24,8 +23,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
private int countMeh;
|
private int countMeh;
|
||||||
private int countMiss;
|
private int countMiss;
|
||||||
|
|
||||||
public TaikoPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score)
|
public TaikoPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
|
||||||
: base(ruleset, beatmap, score)
|
: base(ruleset, attributes, score)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,32 +52,32 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
|||||||
|
|
||||||
public void SetStrongState(bool state)
|
public void SetStrongState(bool state)
|
||||||
{
|
{
|
||||||
var hits = SelectedHitObjects.OfType<Hit>();
|
var hits = EditorBeatmap.SelectedHitObjects.OfType<Hit>();
|
||||||
|
|
||||||
ChangeHandler.BeginChange();
|
EditorBeatmap.BeginChange();
|
||||||
|
|
||||||
foreach (var h in hits)
|
foreach (var h in hits)
|
||||||
{
|
{
|
||||||
if (h.IsStrong != state)
|
if (h.IsStrong != state)
|
||||||
{
|
{
|
||||||
h.IsStrong = state;
|
h.IsStrong = state;
|
||||||
EditorBeatmap.UpdateHitObject(h);
|
EditorBeatmap.Update(h);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangeHandler.EndChange();
|
EditorBeatmap.EndChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetRimState(bool state)
|
public void SetRimState(bool state)
|
||||||
{
|
{
|
||||||
var hits = SelectedHitObjects.OfType<Hit>();
|
var hits = EditorBeatmap.SelectedHitObjects.OfType<Hit>();
|
||||||
|
|
||||||
ChangeHandler.BeginChange();
|
EditorBeatmap.BeginChange();
|
||||||
|
|
||||||
foreach (var h in hits)
|
foreach (var h in hits)
|
||||||
h.Type = state ? HitType.Rim : HitType.Centre;
|
h.Type = state ? HitType.Rim : HitType.Centre;
|
||||||
|
|
||||||
ChangeHandler.EndChange();
|
EditorBeatmap.EndChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint> selection)
|
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint> selection)
|
||||||
@ -89,12 +89,14 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
|||||||
yield return new TernaryStateMenuItem("Strong") { State = { BindTarget = selectionStrongState } };
|
yield return new TernaryStateMenuItem("Strong") { State = { BindTarget = selectionStrongState } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool HandleMovement(MoveSelectionEvent moveEvent) => true;
|
||||||
|
|
||||||
protected override void UpdateTernaryStates()
|
protected override void UpdateTernaryStates()
|
||||||
{
|
{
|
||||||
base.UpdateTernaryStates();
|
base.UpdateTernaryStates();
|
||||||
|
|
||||||
selectionRimState.Value = GetStateFromSelection(SelectedHitObjects.OfType<Hit>(), h => h.Type == HitType.Rim);
|
selectionRimState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<Hit>(), h => h.Type == HitType.Rim);
|
||||||
selectionStrongState.Value = GetStateFromSelection(SelectedHitObjects.OfType<TaikoHitObject>(), h => h.IsStrong);
|
selectionStrongState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<TaikoHitObject>(), h => h.IsStrong);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,7 +158,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
{
|
{
|
||||||
base.LoadSamples();
|
base.LoadSamples();
|
||||||
|
|
||||||
isStrong.Value = getStrongSamples().Any();
|
if (HitObject.CanBeStrong)
|
||||||
|
isStrong.Value = getStrongSamples().Any();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSamplesFromStrong()
|
private void updateSamplesFromStrong()
|
||||||
|
@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
|||||||
set => Duration = value - StartTime;
|
set => Duration = value - StartTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool CanBeStrong => false;
|
||||||
|
|
||||||
public double Duration { get; set; }
|
public double Duration { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
@ -30,6 +31,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
|||||||
|
|
||||||
public readonly Bindable<bool> IsStrongBindable = new BindableBool();
|
public readonly Bindable<bool> IsStrongBindable = new BindableBool();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this <see cref="TaikoHitObject"/> can be made a "strong" (large) hit.
|
||||||
|
/// </summary>
|
||||||
|
public virtual bool CanBeStrong => true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this HitObject is a "strong" type.
|
/// Whether this HitObject is a "strong" type.
|
||||||
/// Strong hit objects give more points for hitting the hit object with both keys.
|
/// Strong hit objects give more points for hitting the hit object with both keys.
|
||||||
@ -37,7 +43,13 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
|||||||
public bool IsStrong
|
public bool IsStrong
|
||||||
{
|
{
|
||||||
get => IsStrongBindable.Value;
|
get => IsStrongBindable.Value;
|
||||||
set => IsStrongBindable.Value = value;
|
set
|
||||||
|
{
|
||||||
|
if (value && !CanBeStrong)
|
||||||
|
throw new InvalidOperationException($"Object of type {GetType()} cannot be strong");
|
||||||
|
|
||||||
|
IsStrongBindable.Value = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
|
@ -7,6 +7,7 @@ using osu.Framework.Audio.Sample;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.Taiko.UI;
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
@ -14,13 +15,29 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
|||||||
{
|
{
|
||||||
public class TaikoLegacySkinTransformer : LegacySkinTransformer
|
public class TaikoLegacySkinTransformer : LegacySkinTransformer
|
||||||
{
|
{
|
||||||
|
private Lazy<bool> hasExplosion;
|
||||||
|
|
||||||
public TaikoLegacySkinTransformer(ISkinSource source)
|
public TaikoLegacySkinTransformer(ISkinSource source)
|
||||||
: base(source)
|
: base(source)
|
||||||
{
|
{
|
||||||
|
Source.SourceChanged += sourceChanged;
|
||||||
|
sourceChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sourceChanged()
|
||||||
|
{
|
||||||
|
hasExplosion = new Lazy<bool>(() => Source.GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Drawable GetDrawableComponent(ISkinComponent component)
|
public override Drawable GetDrawableComponent(ISkinComponent component)
|
||||||
{
|
{
|
||||||
|
if (component is GameplaySkinComponent<HitResult>)
|
||||||
|
{
|
||||||
|
// if a taiko skin is providing explosion sprites, hide the judgements completely
|
||||||
|
if (hasExplosion.Value)
|
||||||
|
return Drawable.Empty();
|
||||||
|
}
|
||||||
|
|
||||||
if (!(component is TaikoSkinComponent taikoComponent))
|
if (!(component is TaikoSkinComponent taikoComponent))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@ -87,10 +104,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
|||||||
|
|
||||||
var hitName = getHitName(taikoComponent.Component);
|
var hitName = getHitName(taikoComponent.Component);
|
||||||
var hitSprite = this.GetAnimation(hitName, true, false);
|
var hitSprite = this.GetAnimation(hitName, true, false);
|
||||||
var strongHitSprite = this.GetAnimation($"{hitName}k", true, false);
|
|
||||||
|
|
||||||
if (hitSprite != null)
|
if (hitSprite != null)
|
||||||
|
{
|
||||||
|
var strongHitSprite = this.GetAnimation($"{hitName}k", true, false);
|
||||||
|
|
||||||
return new LegacyHitExplosion(hitSprite, strongHitSprite);
|
return new LegacyHitExplosion(hitSprite, strongHitSprite);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
@ -153,12 +153,39 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
|
|
||||||
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(this, beatmap);
|
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(this, beatmap);
|
||||||
|
|
||||||
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new TaikoPerformanceCalculator(this, beatmap, score);
|
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new TaikoPerformanceCalculator(this, attributes, score);
|
||||||
|
|
||||||
public int LegacyID => 1;
|
public int LegacyID => 1;
|
||||||
|
|
||||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
|
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
|
||||||
|
|
||||||
|
protected override IEnumerable<HitResult> GetValidHitResults()
|
||||||
|
{
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
HitResult.Great,
|
||||||
|
HitResult.Ok,
|
||||||
|
|
||||||
|
HitResult.SmallTickHit,
|
||||||
|
|
||||||
|
HitResult.SmallBonus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string GetDisplayNameForHitResult(HitResult result)
|
||||||
|
{
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case HitResult.SmallTickHit:
|
||||||
|
return "drum tick";
|
||||||
|
|
||||||
|
case HitResult.SmallBonus:
|
||||||
|
return "strong bonus";
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.GetDisplayNameForHitResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
|
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
|
||||||
{
|
{
|
||||||
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList();
|
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList();
|
||||||
|
@ -163,16 +163,14 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
target = centreHit;
|
target = centreHit;
|
||||||
back = centre;
|
back = centre;
|
||||||
|
|
||||||
if (gameplayClock?.IsSeeking != true)
|
drumSample.Centre?.Play();
|
||||||
drumSample.Centre?.Play();
|
|
||||||
}
|
}
|
||||||
else if (action == RimAction)
|
else if (action == RimAction)
|
||||||
{
|
{
|
||||||
target = rimHit;
|
target = rimHit;
|
||||||
back = rim;
|
back = rim;
|
||||||
|
|
||||||
if (gameplayClock?.IsSeeking != true)
|
drumSample.Rim?.Play();
|
||||||
drumSample.Rim?.Play();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target != null)
|
if (target != null)
|
||||||
|
@ -28,5 +28,28 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
|
|
||||||
Assert.That(key1, Is.EqualTo(key2));
|
Assert.That(key1, Is.EqualTo(key2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase(1.3, DifficultyRating.Easy)]
|
||||||
|
[TestCase(1.993, DifficultyRating.Easy)]
|
||||||
|
[TestCase(1.998, DifficultyRating.Normal)]
|
||||||
|
[TestCase(2.4, DifficultyRating.Normal)]
|
||||||
|
[TestCase(2.693, DifficultyRating.Normal)]
|
||||||
|
[TestCase(2.698, DifficultyRating.Hard)]
|
||||||
|
[TestCase(3.5, DifficultyRating.Hard)]
|
||||||
|
[TestCase(3.993, DifficultyRating.Hard)]
|
||||||
|
[TestCase(3.997, DifficultyRating.Insane)]
|
||||||
|
[TestCase(5.0, DifficultyRating.Insane)]
|
||||||
|
[TestCase(5.292, DifficultyRating.Insane)]
|
||||||
|
[TestCase(5.297, DifficultyRating.Expert)]
|
||||||
|
[TestCase(6.2, DifficultyRating.Expert)]
|
||||||
|
[TestCase(6.493, DifficultyRating.Expert)]
|
||||||
|
[TestCase(6.498, DifficultyRating.ExpertPlus)]
|
||||||
|
[TestCase(8.3, DifficultyRating.ExpertPlus)]
|
||||||
|
public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket)
|
||||||
|
{
|
||||||
|
var actualBracket = BeatmapDifficultyManager.GetDifficultyRating(starRating);
|
||||||
|
|
||||||
|
Assert.AreEqual(expectedBracket, actualBracket);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -651,5 +651,63 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
Assert.IsInstanceOf<LegacyDifficultyCalculatorBeatmapDecoder>(decoder);
|
Assert.IsInstanceOf<LegacyDifficultyCalculatorBeatmapDecoder>(decoder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultiSegmentSliders()
|
||||||
|
{
|
||||||
|
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||||
|
|
||||||
|
using (var resStream = TestResources.OpenResource("multi-segment-slider.osu"))
|
||||||
|
using (var stream = new LineBufferedReader(resStream))
|
||||||
|
{
|
||||||
|
var decoded = decoder.Decode(stream);
|
||||||
|
|
||||||
|
// Multi-segment
|
||||||
|
var first = ((IHasPath)decoded.HitObjects[0]).Path;
|
||||||
|
|
||||||
|
Assert.That(first.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
|
||||||
|
Assert.That(first.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve));
|
||||||
|
Assert.That(first.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244)));
|
||||||
|
Assert.That(first.ControlPoints[1].Type.Value, Is.EqualTo(null));
|
||||||
|
|
||||||
|
Assert.That(first.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3)));
|
||||||
|
Assert.That(first.ControlPoints[2].Type.Value, Is.EqualTo(PathType.Bezier));
|
||||||
|
Assert.That(first.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(68, 15)));
|
||||||
|
Assert.That(first.ControlPoints[3].Type.Value, Is.EqualTo(null));
|
||||||
|
Assert.That(first.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(259, -132)));
|
||||||
|
Assert.That(first.ControlPoints[4].Type.Value, Is.EqualTo(null));
|
||||||
|
Assert.That(first.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(92, -107)));
|
||||||
|
Assert.That(first.ControlPoints[5].Type.Value, Is.EqualTo(null));
|
||||||
|
|
||||||
|
// Single-segment
|
||||||
|
var second = ((IHasPath)decoded.HitObjects[1]).Path;
|
||||||
|
|
||||||
|
Assert.That(second.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
|
||||||
|
Assert.That(second.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve));
|
||||||
|
Assert.That(second.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244)));
|
||||||
|
Assert.That(second.ControlPoints[1].Type.Value, Is.EqualTo(null));
|
||||||
|
Assert.That(second.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3)));
|
||||||
|
Assert.That(second.ControlPoints[2].Type.Value, Is.EqualTo(null));
|
||||||
|
|
||||||
|
// Implicit multi-segment
|
||||||
|
var third = ((IHasPath)decoded.HitObjects[2]).Path;
|
||||||
|
|
||||||
|
Assert.That(third.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
|
||||||
|
Assert.That(third.ControlPoints[0].Type.Value, Is.EqualTo(PathType.Bezier));
|
||||||
|
Assert.That(third.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(0, 192)));
|
||||||
|
Assert.That(third.ControlPoints[1].Type.Value, Is.EqualTo(null));
|
||||||
|
Assert.That(third.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(224, 192)));
|
||||||
|
Assert.That(third.ControlPoints[2].Type.Value, Is.EqualTo(null));
|
||||||
|
|
||||||
|
Assert.That(third.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(224, 0)));
|
||||||
|
Assert.That(third.ControlPoints[3].Type.Value, Is.EqualTo(PathType.Bezier));
|
||||||
|
Assert.That(third.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(224, -192)));
|
||||||
|
Assert.That(third.ControlPoints[4].Type.Value, Is.EqualTo(null));
|
||||||
|
Assert.That(third.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(480, -192)));
|
||||||
|
Assert.That(third.ControlPoints[5].Type.Value, Is.EqualTo(null));
|
||||||
|
Assert.That(third.ControlPoints[6].Position.Value, Is.EqualTo(new Vector2(480, 0)));
|
||||||
|
Assert.That(third.ControlPoints[6].Type.Value, Is.EqualTo(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
{
|
{
|
||||||
private static readonly DllResourceStore beatmaps_resource_store = TestResources.GetStore();
|
private static readonly DllResourceStore beatmaps_resource_store = TestResources.GetStore();
|
||||||
|
|
||||||
private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu"));
|
private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu", StringComparison.Ordinal));
|
||||||
|
|
||||||
[TestCaseSource(nameof(allBeatmaps))]
|
[TestCaseSource(nameof(allBeatmaps))]
|
||||||
public void TestEncodeDecodeStability(string name)
|
public void TestEncodeDecodeStability(string name)
|
||||||
|
@ -44,7 +44,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
HitObjects =
|
HitObjects =
|
||||||
{
|
{
|
||||||
new HitCircle { StartTime = 1000 }
|
new HitCircle { StartTime = 1000, NewCombo = true }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
current.AddRange(new[]
|
current.AddRange(new[]
|
||||||
{
|
{
|
||||||
new HitCircle { StartTime = 1000 },
|
new HitCircle { StartTime = 1000, NewCombo = true },
|
||||||
new HitCircle { StartTime = 3000 },
|
new HitCircle { StartTime = 3000 },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
current.AddRange(new[]
|
current.AddRange(new[]
|
||||||
{
|
{
|
||||||
new HitCircle { StartTime = 1000 },
|
new HitCircle { StartTime = 1000, NewCombo = true },
|
||||||
new HitCircle { StartTime = 2000 },
|
new HitCircle { StartTime = 2000 },
|
||||||
new HitCircle { StartTime = 3000 },
|
new HitCircle { StartTime = 3000 },
|
||||||
});
|
});
|
||||||
@ -100,7 +100,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
current.AddRange(new[]
|
current.AddRange(new[]
|
||||||
{
|
{
|
||||||
new HitCircle { StartTime = 1000 },
|
new HitCircle { StartTime = 1000, NewCombo = true },
|
||||||
new HitCircle { StartTime = 2000 },
|
new HitCircle { StartTime = 2000 },
|
||||||
new HitCircle { StartTime = 3000 },
|
new HitCircle { StartTime = 3000 },
|
||||||
});
|
});
|
||||||
@ -109,7 +109,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
HitObjects =
|
HitObjects =
|
||||||
{
|
{
|
||||||
new HitCircle { StartTime = 500 },
|
new HitCircle { StartTime = 500, NewCombo = true },
|
||||||
(OsuHitObject)current.HitObjects[1],
|
(OsuHitObject)current.HitObjects[1],
|
||||||
(OsuHitObject)current.HitObjects[2],
|
(OsuHitObject)current.HitObjects[2],
|
||||||
}
|
}
|
||||||
@ -123,7 +123,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
current.AddRange(new[]
|
current.AddRange(new[]
|
||||||
{
|
{
|
||||||
new HitCircle { StartTime = 1000 },
|
new HitCircle { StartTime = 1000, NewCombo = true },
|
||||||
new HitCircle { StartTime = 2000 },
|
new HitCircle { StartTime = 2000 },
|
||||||
new HitCircle { StartTime = 3000 },
|
new HitCircle { StartTime = 3000 },
|
||||||
});
|
});
|
||||||
@ -146,7 +146,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
current.AddRange(new OsuHitObject[]
|
current.AddRange(new OsuHitObject[]
|
||||||
{
|
{
|
||||||
new HitCircle { StartTime = 1000 },
|
new HitCircle { StartTime = 1000, NewCombo = true },
|
||||||
new Slider
|
new Slider
|
||||||
{
|
{
|
||||||
StartTime = 2000,
|
StartTime = 2000,
|
||||||
@ -188,7 +188,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
current.AddRange(new[]
|
current.AddRange(new[]
|
||||||
{
|
{
|
||||||
new HitCircle { StartTime = 1000 },
|
new HitCircle { StartTime = 1000, NewCombo = true },
|
||||||
new HitCircle { StartTime = 2000 },
|
new HitCircle { StartTime = 2000 },
|
||||||
new HitCircle { StartTime = 3000 },
|
new HitCircle { StartTime = 3000 },
|
||||||
});
|
});
|
||||||
@ -197,7 +197,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
HitObjects =
|
HitObjects =
|
||||||
{
|
{
|
||||||
new HitCircle { StartTime = 500 },
|
new HitCircle { StartTime = 500, NewCombo = true },
|
||||||
(OsuHitObject)current.HitObjects[0],
|
(OsuHitObject)current.HitObjects[0],
|
||||||
new HitCircle { StartTime = 1500 },
|
new HitCircle { StartTime = 1500 },
|
||||||
(OsuHitObject)current.HitObjects[1],
|
(OsuHitObject)current.HitObjects[1],
|
||||||
@ -216,7 +216,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
current.AddRange(new[]
|
current.AddRange(new[]
|
||||||
{
|
{
|
||||||
new HitCircle { StartTime = 500 },
|
new HitCircle { StartTime = 500, NewCombo = true },
|
||||||
new HitCircle { StartTime = 1000 },
|
new HitCircle { StartTime = 1000 },
|
||||||
new HitCircle { StartTime = 1500 },
|
new HitCircle { StartTime = 1500 },
|
||||||
new HitCircle { StartTime = 2000 },
|
new HitCircle { StartTime = 2000 },
|
||||||
@ -226,6 +226,9 @@ namespace osu.Game.Tests.Editing
|
|||||||
new HitCircle { StartTime = 3500 },
|
new HitCircle { StartTime = 3500 },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var patchedFirst = (HitCircle)current.HitObjects[1];
|
||||||
|
patchedFirst.NewCombo = true;
|
||||||
|
|
||||||
var patch = new OsuBeatmap
|
var patch = new OsuBeatmap
|
||||||
{
|
{
|
||||||
HitObjects =
|
HitObjects =
|
||||||
@ -244,7 +247,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
current.AddRange(new[]
|
current.AddRange(new[]
|
||||||
{
|
{
|
||||||
new HitCircle { StartTime = 500 },
|
new HitCircle { StartTime = 500, NewCombo = true },
|
||||||
new HitCircle { StartTime = 1000 },
|
new HitCircle { StartTime = 1000 },
|
||||||
new HitCircle { StartTime = 1500 },
|
new HitCircle { StartTime = 1500 },
|
||||||
new HitCircle { StartTime = 2000 },
|
new HitCircle { StartTime = 2000 },
|
||||||
@ -277,7 +280,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
current.AddRange(new[]
|
current.AddRange(new[]
|
||||||
{
|
{
|
||||||
new HitCircle { StartTime = 500 },
|
new HitCircle { StartTime = 500, NewCombo = true },
|
||||||
new HitCircle { StartTime = 1000 },
|
new HitCircle { StartTime = 1000 },
|
||||||
new HitCircle { StartTime = 1500 },
|
new HitCircle { StartTime = 1500 },
|
||||||
new HitCircle { StartTime = 2000 },
|
new HitCircle { StartTime = 2000 },
|
||||||
@ -291,7 +294,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
HitObjects =
|
HitObjects =
|
||||||
{
|
{
|
||||||
new HitCircle { StartTime = 750 },
|
new HitCircle { StartTime = 750, NewCombo = true },
|
||||||
(OsuHitObject)current.HitObjects[1],
|
(OsuHitObject)current.HitObjects[1],
|
||||||
(OsuHitObject)current.HitObjects[4],
|
(OsuHitObject)current.HitObjects[4],
|
||||||
(OsuHitObject)current.HitObjects[5],
|
(OsuHitObject)current.HitObjects[5],
|
||||||
@ -309,20 +312,20 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
current.AddRange(new[]
|
current.AddRange(new[]
|
||||||
{
|
{
|
||||||
new HitCircle { StartTime = 500, Position = new Vector2(50) },
|
new HitCircle { StartTime = 500, Position = new Vector2(50), NewCombo = true },
|
||||||
new HitCircle { StartTime = 500, Position = new Vector2(100) },
|
new HitCircle { StartTime = 500, Position = new Vector2(100), NewCombo = true },
|
||||||
new HitCircle { StartTime = 500, Position = new Vector2(150) },
|
new HitCircle { StartTime = 500, Position = new Vector2(150), NewCombo = true },
|
||||||
new HitCircle { StartTime = 500, Position = new Vector2(200) },
|
new HitCircle { StartTime = 500, Position = new Vector2(200), NewCombo = true },
|
||||||
});
|
});
|
||||||
|
|
||||||
var patch = new OsuBeatmap
|
var patch = new OsuBeatmap
|
||||||
{
|
{
|
||||||
HitObjects =
|
HitObjects =
|
||||||
{
|
{
|
||||||
new HitCircle { StartTime = 500, Position = new Vector2(150) },
|
new HitCircle { StartTime = 500, Position = new Vector2(150), NewCombo = true },
|
||||||
new HitCircle { StartTime = 500, Position = new Vector2(100) },
|
new HitCircle { StartTime = 500, Position = new Vector2(100), NewCombo = true },
|
||||||
new HitCircle { StartTime = 500, Position = new Vector2(50) },
|
new HitCircle { StartTime = 500, Position = new Vector2(50), NewCombo = true },
|
||||||
new HitCircle { StartTime = 500, Position = new Vector2(200) },
|
new HitCircle { StartTime = 500, Position = new Vector2(200), NewCombo = true },
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
100
osu.Game.Tests/Editing/TransactionalCommitComponentTest.cs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Editing
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class TransactionalCommitComponentTest
|
||||||
|
{
|
||||||
|
private TestHandler handler;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
handler = new TestHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCommitTransaction()
|
||||||
|
{
|
||||||
|
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
|
||||||
|
|
||||||
|
handler.BeginChange();
|
||||||
|
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
|
||||||
|
handler.EndChange();
|
||||||
|
|
||||||
|
Assert.That(handler.StateUpdateCount, Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSaveOutsideOfTransactionTriggersUpdates()
|
||||||
|
{
|
||||||
|
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
|
||||||
|
|
||||||
|
handler.SaveState();
|
||||||
|
Assert.That(handler.StateUpdateCount, Is.EqualTo(1));
|
||||||
|
|
||||||
|
handler.SaveState();
|
||||||
|
Assert.That(handler.StateUpdateCount, Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestEventsFire()
|
||||||
|
{
|
||||||
|
int transactionBegan = 0;
|
||||||
|
int transactionEnded = 0;
|
||||||
|
int stateSaved = 0;
|
||||||
|
|
||||||
|
handler.TransactionBegan += () => transactionBegan++;
|
||||||
|
handler.TransactionEnded += () => transactionEnded++;
|
||||||
|
handler.SaveStateTriggered += () => stateSaved++;
|
||||||
|
|
||||||
|
handler.BeginChange();
|
||||||
|
Assert.That(transactionBegan, Is.EqualTo(1));
|
||||||
|
|
||||||
|
handler.EndChange();
|
||||||
|
Assert.That(transactionEnded, Is.EqualTo(1));
|
||||||
|
|
||||||
|
Assert.That(stateSaved, Is.EqualTo(0));
|
||||||
|
handler.SaveState();
|
||||||
|
Assert.That(stateSaved, Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSaveDuringTransactionDoesntTriggerUpdate()
|
||||||
|
{
|
||||||
|
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
|
||||||
|
|
||||||
|
handler.BeginChange();
|
||||||
|
|
||||||
|
handler.SaveState();
|
||||||
|
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
|
||||||
|
|
||||||
|
handler.EndChange();
|
||||||
|
|
||||||
|
Assert.That(handler.StateUpdateCount, Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestEndWithoutBeginThrows()
|
||||||
|
{
|
||||||
|
handler.BeginChange();
|
||||||
|
handler.EndChange();
|
||||||
|
Assert.That(() => handler.EndChange(), Throws.TypeOf<InvalidOperationException>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestHandler : TransactionalCommitComponent
|
||||||
|
{
|
||||||
|
public int StateUpdateCount { get; private set; }
|
||||||
|
|
||||||
|
protected override void UpdateState()
|
||||||
|
{
|
||||||
|
StateUpdateCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -111,6 +111,7 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
|
|
||||||
var osu = LoadOsuIntoHost(host);
|
var osu = LoadOsuIntoHost(host);
|
||||||
var storage = osu.Dependencies.Get<Storage>();
|
var storage = osu.Dependencies.Get<Storage>();
|
||||||
|
var osuStorage = storage as MigratableStorage;
|
||||||
|
|
||||||
// Store the current storage's path. We'll need to refer to this for assertions in the original directory after the migration completes.
|
// Store the current storage's path. We'll need to refer to this for assertions in the original directory after the migration completes.
|
||||||
string originalDirectory = storage.GetFullPath(".");
|
string originalDirectory = storage.GetFullPath(".");
|
||||||
@ -137,13 +138,15 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
Assert.That(!Directory.Exists(Path.Combine(originalDirectory, "test-nested", "cache")));
|
Assert.That(!Directory.Exists(Path.Combine(originalDirectory, "test-nested", "cache")));
|
||||||
Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
|
Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
|
||||||
|
|
||||||
foreach (var file in OsuStorage.IGNORE_FILES)
|
Assert.That(osuStorage, Is.Not.Null);
|
||||||
|
|
||||||
|
foreach (var file in osuStorage.IgnoreFiles)
|
||||||
{
|
{
|
||||||
Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
|
Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
|
||||||
Assert.That(storage.Exists(file), Is.False);
|
Assert.That(storage.Exists(file), Is.False);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var dir in OsuStorage.IGNORE_DIRECTORIES)
|
foreach (var dir in osuStorage.IgnoreDirectories)
|
||||||
{
|
{
|
||||||
Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir)));
|
Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir)));
|
||||||
Assert.That(storage.ExistsDirectory(dir), Is.False);
|
Assert.That(storage.ExistsDirectory(dir), Is.False);
|
||||||
|
@ -94,6 +94,52 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA);
|
Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultiModFlattening()
|
||||||
|
{
|
||||||
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModC())).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
|
Assert.AreEqual(4, combinations.Length);
|
||||||
|
Assert.IsTrue(combinations[0] is ModNoMod);
|
||||||
|
Assert.IsTrue(combinations[1] is ModA);
|
||||||
|
Assert.IsTrue(combinations[2] is MultiMod);
|
||||||
|
Assert.IsTrue(combinations[3] is MultiMod);
|
||||||
|
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[2] is ModC);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[3]).Mods[0] is ModB);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[3]).Mods[1] is ModC);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestIncompatibleThroughMultiMod()
|
||||||
|
{
|
||||||
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModIncompatibleWithA())).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
|
Assert.AreEqual(3, combinations.Length);
|
||||||
|
Assert.IsTrue(combinations[0] is ModNoMod);
|
||||||
|
Assert.IsTrue(combinations[1] is ModA);
|
||||||
|
Assert.IsTrue(combinations[2] is MultiMod);
|
||||||
|
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModB);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModIncompatibleWithA);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestIncompatibleWithSameInstanceViaMultiMod()
|
||||||
|
{
|
||||||
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModA(), new ModB())).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
|
Assert.AreEqual(3, combinations.Length);
|
||||||
|
Assert.IsTrue(combinations[0] is ModNoMod);
|
||||||
|
Assert.IsTrue(combinations[1] is ModA);
|
||||||
|
Assert.IsTrue(combinations[2] is MultiMod);
|
||||||
|
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
|
||||||
|
}
|
||||||
|
|
||||||
private class ModA : Mod
|
private class ModA : Mod
|
||||||
{
|
{
|
||||||
public override string Name => nameof(ModA);
|
public override string Name => nameof(ModA);
|
||||||
@ -112,6 +158,13 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithAAndB) };
|
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithAAndB) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ModC : Mod
|
||||||
|
{
|
||||||
|
public override string Name => nameof(ModC);
|
||||||
|
public override string Acronym => nameof(ModC);
|
||||||
|
public override double ScoreMultiplier => 1;
|
||||||
|
}
|
||||||
|
|
||||||
private class ModIncompatibleWithA : Mod
|
private class ModIncompatibleWithA : Mod
|
||||||
{
|
{
|
||||||
public override string Name => $"Incompatible With {nameof(ModA)}";
|
public override string Name => $"Incompatible With {nameof(ModA)}";
|
||||||
|
@ -197,5 +197,22 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
carouselItem.Filter(criteria);
|
carouselItem.Filter(criteria);
|
||||||
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("202010", true)]
|
||||||
|
[TestCase("20201010", false)]
|
||||||
|
[TestCase("153", true)]
|
||||||
|
[TestCase("1535", false)]
|
||||||
|
public void TestCriteriaMatchingBeatmapIDs(string query, bool filtered)
|
||||||
|
{
|
||||||
|
var beatmap = getExampleBeatmap();
|
||||||
|
beatmap.OnlineBeatmapID = 20201010;
|
||||||
|
beatmap.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = 1535 };
|
||||||
|
|
||||||
|
var criteria = new FilterCriteria { SearchText = query };
|
||||||
|
var carouselItem = new CarouselBeatmap(beatmap);
|
||||||
|
carouselItem.Filter(criteria);
|
||||||
|
|
||||||
|
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
osu.Game.Tests/Resources/multi-segment-slider.osu
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
osu file format v128
|
||||||
|
|
||||||
|
[HitObjects]
|
||||||
|
// Multi-segment
|
||||||
|
63,301,1000,6,0,P|224:57|B|439:298|131:316|322:169|155:194,1,1040,0|0,0:0|0:0,0:0:0:0:
|
||||||
|
|
||||||
|
// Single-segment
|
||||||
|
63,301,2000,6,0,P|224:57|439:298,1,1040,0|0,0:0|0:0,0:0:0:0:
|
||||||
|
|
||||||
|
// Implicit multi-segment
|
||||||
|
32,192,3000,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800
|
BIN
osu.Game.Tests/Resources/old-skin/score-0.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-1.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-2.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-3.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-4.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-5.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-6.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-7.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-8.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-9.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-comma.png
Normal file
After Width: | Height: | Size: 865 B |
BIN
osu.Game.Tests/Resources/old-skin/score-dot.png
Normal file
After Width: | Height: | Size: 771 B |
BIN
osu.Game.Tests/Resources/old-skin/score-percent.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-x.png
Normal file
After Width: | Height: | Size: 2.5 KiB |