mirror of
https://github.com/ppy/osu.git
synced 2026-05-19 06:31:19 +08:00
Compare commits
1031 Commits
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"jetbrains.resharper.globaltools": {
|
||||
"version": "2022.1.1",
|
||||
"version": "2022.2.3",
|
||||
"commands": [
|
||||
"jb"
|
||||
]
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
on: [push, pull_request]
|
||||
name: Continuous Integration
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
inspect-code:
|
||||
|
||||
@@ -6,8 +6,6 @@ T:System.IComparable;Don't use non-generic IComparable. Use generic version inst
|
||||
T:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable<T> instead.
|
||||
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
|
||||
M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
|
||||
T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
|
||||
T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.
|
||||
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
|
||||
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
|
||||
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead.
|
||||
|
||||
-1
@@ -12,7 +12,6 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
|
||||
|
||||
+3
@@ -77,5 +77,8 @@ namespace osu.Game.Rulesets.EmptyFreeform
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Leave this line intact. It will bake the correct version into the ruleset on each build/release.
|
||||
public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -12,7 +12,6 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||
|
||||
@@ -49,5 +49,8 @@ namespace osu.Game.Rulesets.Pippidon
|
||||
};
|
||||
|
||||
public override Drawable CreateIcon() => new PippidonRulesetIcon(this);
|
||||
|
||||
// Leave this line intact. It will bake the correct version into the ruleset on each build/release.
|
||||
public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -12,7 +12,6 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
||||
|
||||
+3
@@ -54,5 +54,8 @@ namespace osu.Game.Rulesets.EmptyScrolling
|
||||
Text = ShortName[0].ToString(),
|
||||
Font = OsuFont.Default.With(size: 18),
|
||||
};
|
||||
|
||||
// Leave this line intact. It will bake the correct version into the ruleset on each build/release.
|
||||
public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -12,7 +12,6 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||
|
||||
+5
-2
@@ -21,8 +21,11 @@ namespace osu.Game.Rulesets.Pippidon.Beatmaps
|
||||
public PippidonBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
|
||||
: base(beatmap, ruleset)
|
||||
{
|
||||
minPosition = beatmap.HitObjects.Min(getUsablePosition);
|
||||
maxPosition = beatmap.HitObjects.Max(getUsablePosition);
|
||||
if (beatmap.HitObjects.Any())
|
||||
{
|
||||
minPosition = beatmap.HitObjects.Min(getUsablePosition);
|
||||
maxPosition = beatmap.HitObjects.Max(getUsablePosition);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition && h is IHasYPosition);
|
||||
|
||||
+3
@@ -46,5 +46,8 @@ namespace osu.Game.Rulesets.Pippidon
|
||||
};
|
||||
|
||||
public override Drawable CreateIcon() => new PippidonRulesetIcon(this);
|
||||
|
||||
// Leave this line intact. It will bake the correct version into the ruleset on each build/release.
|
||||
public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -51,8 +51,8 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.819.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.819.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.831.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.916.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace osu.Desktop.Security
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Icon = FontAwesome.Solid.ShieldAlt;
|
||||
IconBackground.Colour = colours.YellowDark;
|
||||
IconContent.Colour = colours.YellowDark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,29 +5,24 @@ using System;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using Squirrel;
|
||||
using Squirrel.SimpleSplat;
|
||||
using LogLevel = Squirrel.SimpleSplat.LogLevel;
|
||||
using UpdateManager = osu.Game.Updater.UpdateManager;
|
||||
|
||||
namespace osu.Desktop.Updater
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager
|
||||
public class SquirrelUpdateManager : UpdateManager
|
||||
{
|
||||
private UpdateManager? updateManager;
|
||||
private Squirrel.UpdateManager? updateManager;
|
||||
private INotificationOverlay notificationOverlay = null!;
|
||||
|
||||
public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited();
|
||||
public Task PrepareUpdateAsync() => Squirrel.UpdateManager.RestartAppWhenExited();
|
||||
|
||||
private static readonly Logger logger = Logger.GetLogger("updater");
|
||||
|
||||
@@ -38,6 +33,9 @@ namespace osu.Desktop.Updater
|
||||
|
||||
private readonly SquirrelLogger squirrelLogger = new SquirrelLogger();
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(INotificationOverlay notifications)
|
||||
{
|
||||
@@ -66,7 +64,14 @@ namespace osu.Desktop.Updater
|
||||
if (updatePending)
|
||||
{
|
||||
// the user may have dismissed the completion notice, so show it again.
|
||||
notificationOverlay.Post(new UpdateCompleteNotification(this));
|
||||
notificationOverlay.Post(new UpdateApplicationCompleteNotification
|
||||
{
|
||||
Activated = () =>
|
||||
{
|
||||
restartToApplyUpdate();
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -78,19 +83,21 @@ namespace osu.Desktop.Updater
|
||||
|
||||
if (notification == null)
|
||||
{
|
||||
notification = new UpdateProgressNotification(this) { State = ProgressNotificationState.Active };
|
||||
notification = new UpdateProgressNotification
|
||||
{
|
||||
CompletionClickAction = restartToApplyUpdate,
|
||||
};
|
||||
|
||||
Schedule(() => notificationOverlay.Post(notification));
|
||||
}
|
||||
|
||||
notification.Progress = 0;
|
||||
notification.Text = @"Downloading update...";
|
||||
notification.StartDownload();
|
||||
|
||||
try
|
||||
{
|
||||
await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
||||
|
||||
notification.Progress = 0;
|
||||
notification.Text = @"Installing update...";
|
||||
notification.StartInstall();
|
||||
|
||||
await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
||||
|
||||
@@ -110,9 +117,7 @@ namespace osu.Desktop.Updater
|
||||
else
|
||||
{
|
||||
// In the case of an error, a separate notification will be displayed.
|
||||
notification.State = ProgressNotificationState.Cancelled;
|
||||
notification.Close();
|
||||
|
||||
notification.FailDownload();
|
||||
Logger.Error(e, @"update failed!");
|
||||
}
|
||||
}
|
||||
@@ -134,84 +139,24 @@ namespace osu.Desktop.Updater
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool restartToApplyUpdate()
|
||||
{
|
||||
PrepareUpdateAsync()
|
||||
.ContinueWith(_ => Schedule(() => game.AttemptExit()));
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
updateManager?.Dispose();
|
||||
}
|
||||
|
||||
private class UpdateCompleteNotification : ProgressCompletionNotification
|
||||
{
|
||||
[Resolved]
|
||||
private OsuGame game { get; set; } = null!;
|
||||
|
||||
public UpdateCompleteNotification(SquirrelUpdateManager updateManager)
|
||||
{
|
||||
Text = @"Update ready to install. Click to restart!";
|
||||
|
||||
Activated = () =>
|
||||
{
|
||||
updateManager.PrepareUpdateAsync()
|
||||
.ContinueWith(_ => updateManager.Schedule(() => game.AttemptExit()));
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateProgressNotification : ProgressNotification
|
||||
{
|
||||
private readonly SquirrelUpdateManager updateManager;
|
||||
|
||||
public UpdateProgressNotification(SquirrelUpdateManager updateManager)
|
||||
{
|
||||
this.updateManager = updateManager;
|
||||
}
|
||||
|
||||
protected override Notification CreateCompletionNotification()
|
||||
{
|
||||
return new UpdateCompleteNotification(updateManager);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
IconContent.AddRange(new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientVertical(colours.YellowDark, colours.Yellow)
|
||||
},
|
||||
new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Icon = FontAwesome.Solid.Upload,
|
||||
Colour = Color4.White,
|
||||
Size = new Vector2(20),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
// cancelling updates is not currently supported by the underlying updater.
|
||||
// only allow dismissing for now.
|
||||
|
||||
switch (State)
|
||||
{
|
||||
case ProgressNotificationState.Cancelled:
|
||||
base.Close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SquirrelLogger : ILogger, IDisposable
|
||||
{
|
||||
public Squirrel.SimpleSplat.LogLevel Level { get; set; } = Squirrel.SimpleSplat.LogLevel.Info;
|
||||
public LogLevel Level { get; set; } = LogLevel.Info;
|
||||
|
||||
public void Write(string message, Squirrel.SimpleSplat.LogLevel logLevel)
|
||||
public void Write(string message, LogLevel logLevel)
|
||||
{
|
||||
if (logLevel < Level)
|
||||
return;
|
||||
|
||||
@@ -27,11 +27,6 @@
|
||||
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
|
||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.14">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.0.175" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
|
||||
@@ -70,10 +70,17 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
[Cached]
|
||||
private readonly BindableBeatDivisor beatDivisor;
|
||||
|
||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
public EditorBeatmapDependencyContainer(IBeatmap beatmap, BindableBeatDivisor beatDivisor)
|
||||
{
|
||||
editorClock = new EditorClock(beatmap, beatDivisor);
|
||||
this.beatDivisor = beatDivisor;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
editorClock = new EditorClock(beatmap, beatDivisor),
|
||||
Content,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneCatchTouchInput : OsuTestScene
|
||||
{
|
||||
private CatchTouchInputMapper catchTouchInputMapper = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create input overlay", () =>
|
||||
{
|
||||
Child = new CatchInputManager(new CatchRuleset().RulesetInfo)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
catchTouchInputMapper = new CatchTouchInputMapper
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("show overlay", () => catchTouchInputMapper.Show());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch
|
||||
{
|
||||
[Cached]
|
||||
public class CatchInputManager : RulesetInputManager<CatchAction>
|
||||
{
|
||||
public CatchInputManager(RulesetInfo ruleset)
|
||||
|
||||
@@ -1,38 +1,37 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Difficulty;
|
||||
using osu.Game.Rulesets.Catch.Scoring;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using System;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Game.Rulesets.Catch.Edit;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
using osu.Game.Rulesets.Catch.Scoring;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Legacy;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch
|
||||
{
|
||||
public class CatchRuleset : Ruleset, ILegacyRuleset
|
||||
{
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableCatchRuleset(this, beatmap, mods);
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableCatchRuleset(this, beatmap, mods);
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor();
|
||||
|
||||
@@ -42,6 +41,8 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
public const string SHORT_NAME = "fruits";
|
||||
|
||||
public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION;
|
||||
|
||||
public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0) => new[]
|
||||
{
|
||||
new KeyBinding(InputKey.Z, CatchAction.MoveLeft),
|
||||
@@ -162,7 +163,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
};
|
||||
}
|
||||
|
||||
public override string GetDisplayNameForHitResult(HitResult result)
|
||||
public override LocalisableString GetDisplayNameForHitResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
@@ -181,7 +182,16 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new CatchDifficultyCalculator(RulesetInfo, beatmap);
|
||||
|
||||
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new CatchLegacySkinTransformer(skin);
|
||||
public override ISkin? CreateSkinTransformer(ISkin skin, IBeatmap beatmap)
|
||||
{
|
||||
switch (skin)
|
||||
{
|
||||
case LegacySkin:
|
||||
return new CatchLegacySkinTransformer(skin);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override PerformanceCalculator CreatePerformanceCalculator() => new CatchPerformanceCalculator();
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
|
||||
{
|
||||
while (path.Vertices.Count < InternalChildren.Count)
|
||||
RemoveInternal(InternalChildren[^1]);
|
||||
RemoveInternal(InternalChildren[^1], true);
|
||||
|
||||
while (InternalChildren.Count < path.Vertices.Count)
|
||||
AddInternal(new VertexPiece());
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
.Where(h => !(h is TinyDroplet)));
|
||||
|
||||
while (nestedHitObjects.Count < InternalChildren.Count)
|
||||
RemoveInternal(InternalChildren[^1]);
|
||||
RemoveInternal(InternalChildren[^1], true);
|
||||
|
||||
while (InternalChildren.Count < nestedHitObjects.Count)
|
||||
AddInternal(new FruitOutline());
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModEasy : ModEasyWithExtraLives
|
||||
{
|
||||
public override string Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
|
||||
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public override string Name => "Floating Fruits";
|
||||
public override string Acronym => "FF";
|
||||
public override string Description => "The fruits are... floating?";
|
||||
public override LocalisableString Description => "The fruits are... floating?";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Cloud;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
@@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset<CatchHitObject>
|
||||
{
|
||||
public override string Description => @"Play with fading fruits.";
|
||||
public override LocalisableString Description => @"Play with fading fruits.";
|
||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
|
||||
|
||||
private const double fade_out_offset_multiplier = 0.6;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
@@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModMirror : ModMirror, IApplicableToBeatmap
|
||||
{
|
||||
public override string Description => "Fruits are flipped horizontally.";
|
||||
public override LocalisableString Description => "Fruits are flipped horizontally.";
|
||||
|
||||
/// <remarks>
|
||||
/// <see cref="IApplicableToBeatmap"/> is used instead of <see cref="IApplicableToHitObject"/>,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
@@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModNoScope : ModNoScope, IUpdatableByPlayfield
|
||||
{
|
||||
public override string Description => "Where's the catcher?";
|
||||
public override LocalisableString Description => "Where's the catcher?";
|
||||
|
||||
[SettingSource(
|
||||
"Hidden at combo",
|
||||
|
||||
@@ -5,6 +5,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset<CatchHitObject>, IApplicableToPlayer
|
||||
{
|
||||
public override string Description => @"Use the mouse to control the catcher.";
|
||||
public override LocalisableString Description => @"Use the mouse to control the catcher.";
|
||||
|
||||
private DrawableRuleset<CatchHitObject> drawableRuleset = null!;
|
||||
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
public class CatchTouchInputMapper : VisibilityContainer
|
||||
{
|
||||
public override bool PropagatePositionalInputSubTree => true;
|
||||
public override bool PropagateNonPositionalInputSubTree => true;
|
||||
|
||||
private readonly Dictionary<object, TouchCatchAction> trackedActionSources = new Dictionary<object, TouchCatchAction>();
|
||||
|
||||
private KeyBindingContainer<CatchAction> keyBindingContainer = null!;
|
||||
|
||||
private Container mainContent = null!;
|
||||
|
||||
private InputArea leftBox = null!;
|
||||
private InputArea rightBox = null!;
|
||||
private InputArea leftDashBox = null!;
|
||||
private InputArea rightDashBox = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(CatchInputManager catchInputManager, OsuColour colours)
|
||||
{
|
||||
const float width = 0.15f;
|
||||
|
||||
keyBindingContainer = catchInputManager.KeyBindingContainer;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
mainContent = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = width,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
leftDashBox = new InputArea(TouchCatchAction.DashLeft, trackedActionSources)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
},
|
||||
leftBox = new InputArea(TouchCatchAction.MoveLeft, trackedActionSources)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
Colour = colours.Gray9,
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
},
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = width,
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
rightBox = new InputArea(TouchCatchAction.MoveRight, trackedActionSources)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
Colour = colours.Gray9,
|
||||
},
|
||||
rightDashBox = new InputArea(TouchCatchAction.DashRight, trackedActionSources)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
// Hide whenever the keyboard is used.
|
||||
Hide();
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
return updateAction(e.Button, getTouchCatchActionFromInput(e.ScreenSpaceMousePosition));
|
||||
}
|
||||
|
||||
protected override bool OnTouchDown(TouchDownEvent e)
|
||||
{
|
||||
return updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
|
||||
}
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
Show();
|
||||
|
||||
TouchCatchAction? action = getTouchCatchActionFromInput(e.ScreenSpaceMousePosition);
|
||||
|
||||
// multiple mouse buttons may be pressed and handling the same action.
|
||||
foreach (MouseButton button in e.PressedButtons)
|
||||
updateAction(button, action);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnTouchMove(TouchMoveEvent e)
|
||||
{
|
||||
updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
|
||||
base.OnTouchMove(e);
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
updateAction(e.Button, null);
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
protected override void OnTouchUp(TouchUpEvent e)
|
||||
{
|
||||
updateAction(e.Touch.Source, null);
|
||||
base.OnTouchUp(e);
|
||||
}
|
||||
|
||||
private bool updateAction(object source, TouchCatchAction? newAction)
|
||||
{
|
||||
TouchCatchAction? actionBefore = null;
|
||||
|
||||
if (trackedActionSources.TryGetValue(source, out TouchCatchAction found))
|
||||
actionBefore = found;
|
||||
|
||||
if (actionBefore != newAction)
|
||||
{
|
||||
if (newAction != null)
|
||||
trackedActionSources[source] = newAction.Value;
|
||||
else
|
||||
trackedActionSources.Remove(source);
|
||||
|
||||
updatePressedActions();
|
||||
}
|
||||
|
||||
return newAction != null;
|
||||
}
|
||||
|
||||
private void updatePressedActions()
|
||||
{
|
||||
Show();
|
||||
|
||||
if (trackedActionSources.ContainsValue(TouchCatchAction.DashLeft) || trackedActionSources.ContainsValue(TouchCatchAction.MoveLeft))
|
||||
keyBindingContainer.TriggerPressed(CatchAction.MoveLeft);
|
||||
else
|
||||
keyBindingContainer.TriggerReleased(CatchAction.MoveLeft);
|
||||
|
||||
if (trackedActionSources.ContainsValue(TouchCatchAction.DashRight) || trackedActionSources.ContainsValue(TouchCatchAction.MoveRight))
|
||||
keyBindingContainer.TriggerPressed(CatchAction.MoveRight);
|
||||
else
|
||||
keyBindingContainer.TriggerReleased(CatchAction.MoveRight);
|
||||
|
||||
if (trackedActionSources.ContainsValue(TouchCatchAction.DashLeft) || trackedActionSources.ContainsValue(TouchCatchAction.DashRight))
|
||||
keyBindingContainer.TriggerPressed(CatchAction.Dash);
|
||||
else
|
||||
keyBindingContainer.TriggerReleased(CatchAction.Dash);
|
||||
}
|
||||
|
||||
private TouchCatchAction? getTouchCatchActionFromInput(Vector2 screenSpaceInputPosition)
|
||||
{
|
||||
if (leftDashBox.Contains(screenSpaceInputPosition))
|
||||
return TouchCatchAction.DashLeft;
|
||||
if (rightDashBox.Contains(screenSpaceInputPosition))
|
||||
return TouchCatchAction.DashRight;
|
||||
if (leftBox.Contains(screenSpaceInputPosition))
|
||||
return TouchCatchAction.MoveLeft;
|
||||
if (rightBox.Contains(screenSpaceInputPosition))
|
||||
return TouchCatchAction.MoveRight;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override void PopIn() => mainContent.FadeIn(300, Easing.OutQuint);
|
||||
|
||||
protected override void PopOut() => mainContent.FadeOut(300, Easing.OutQuint);
|
||||
|
||||
private class InputArea : CompositeDrawable, IKeyBindingHandler<CatchAction>
|
||||
{
|
||||
private readonly TouchCatchAction handledAction;
|
||||
|
||||
private readonly Box highlightOverlay;
|
||||
|
||||
private readonly IEnumerable<KeyValuePair<object, TouchCatchAction>> trackedActions;
|
||||
|
||||
private bool isHighlighted;
|
||||
|
||||
public InputArea(TouchCatchAction handledAction, IEnumerable<KeyValuePair<object, TouchCatchAction>> trackedActions)
|
||||
{
|
||||
this.handledAction = handledAction;
|
||||
this.trackedActions = trackedActions;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 10,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.15f,
|
||||
},
|
||||
highlightOverlay = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
Blending = BlendingParameters.Additive,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<CatchAction> _)
|
||||
{
|
||||
updateHighlight();
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<CatchAction> _)
|
||||
{
|
||||
updateHighlight();
|
||||
}
|
||||
|
||||
private void updateHighlight()
|
||||
{
|
||||
bool isHandling = trackedActions.Any(a => a.Value == handledAction);
|
||||
|
||||
if (isHandling == isHighlighted)
|
||||
return;
|
||||
|
||||
isHighlighted = isHandling;
|
||||
highlightOverlay.FadeTo(isHighlighted ? 0.1f : 0, isHighlighted ? 80 : 400, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
public enum TouchCatchAction
|
||||
{
|
||||
MoveLeft,
|
||||
MoveRight,
|
||||
DashLeft,
|
||||
DashRight,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,8 +271,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
SetHyperDashState();
|
||||
}
|
||||
|
||||
caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject);
|
||||
droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject);
|
||||
caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject, false);
|
||||
droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -430,7 +430,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
var droppedObject = getDroppedObject(caughtObject);
|
||||
|
||||
caughtObjectContainer.Remove(caughtObject);
|
||||
caughtObjectContainer.Remove(caughtObject, false);
|
||||
|
||||
droppedObjectTarget.Add(droppedObject);
|
||||
|
||||
|
||||
@@ -93,15 +93,15 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
switch (entry.Animation)
|
||||
{
|
||||
case CatcherTrailAnimation.Dashing:
|
||||
dashTrails.Remove(drawable);
|
||||
dashTrails.Remove(drawable, false);
|
||||
break;
|
||||
|
||||
case CatcherTrailAnimation.HyperDashing:
|
||||
hyperDashTrails.Remove(drawable);
|
||||
hyperDashTrails.Remove(drawable, false);
|
||||
break;
|
||||
|
||||
case CatcherTrailAnimation.HyperDashAfterImage:
|
||||
hyperDashAfterImages.Remove(drawable);
|
||||
hyperDashAfterImages.Remove(drawable, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
@@ -32,6 +33,12 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
KeyBindingInputManager.Add(new CatchTouchInputMapper());
|
||||
}
|
||||
|
||||
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
|
||||
|
||||
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);
|
||||
|
||||
@@ -10,6 +10,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@@ -34,10 +35,14 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
AddStep("setup compose screen", () =>
|
||||
{
|
||||
var editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 4 })
|
||||
{
|
||||
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
|
||||
});
|
||||
};
|
||||
|
||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
|
||||
|
||||
var editorBeatmap = new EditorBeatmap(beatmap, new LegacyBeatmapSkin(beatmap.BeatmapInfo, null));
|
||||
|
||||
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
|
||||
|
||||
@@ -50,7 +55,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
(typeof(IBeatSnapProvider), editorBeatmap),
|
||||
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)),
|
||||
},
|
||||
Child = new ComposeScreen { State = { Value = Visibility.Visible } },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
editorBeatmap,
|
||||
new ComposeScreen { State = { Value = Visibility.Visible } },
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
BeatDivisor.Value = 8;
|
||||
Clock.Seek(0);
|
||||
EditorClock.Seek(0);
|
||||
|
||||
Child = composer = new TestComposer { RelativeSizeAxes = Axes.Both };
|
||||
});
|
||||
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
|
||||
originalTime = lastObject.HitObject.StartTime;
|
||||
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
|
||||
EditorClock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
|
||||
});
|
||||
|
||||
AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
|
||||
@@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
|
||||
originalTime = lastObject.HitObject.StartTime;
|
||||
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
|
||||
EditorClock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
|
||||
});
|
||||
|
||||
AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
|
||||
@@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
AddStep("seek to last object", () =>
|
||||
{
|
||||
lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
|
||||
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
|
||||
EditorClock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
|
||||
});
|
||||
|
||||
AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Configuration.Tracking;
|
||||
using osu.Game.Configuration;
|
||||
@@ -13,7 +11,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
{
|
||||
public class ManiaRulesetConfigManager : RulesetConfigManager<ManiaRulesetSetting>
|
||||
{
|
||||
public ManiaRulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null)
|
||||
public ManiaRulesetConfigManager(SettingsStore? settings, RulesetInfo ruleset, int? variant = null)
|
||||
: base(settings, ruleset, variant)
|
||||
{
|
||||
}
|
||||
@@ -33,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
scrollTime => new SettingDescription(
|
||||
rawValue: scrollTime,
|
||||
name: "Scroll Speed",
|
||||
value: $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)} ({scrollTime}ms)"
|
||||
value: $"{scrollTime}ms (speed {(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)})"
|
||||
)
|
||||
)
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
private readonly bool isForCurrentRuleset;
|
||||
private readonly double originalOverallDifficulty;
|
||||
|
||||
public override int Version => 20220701;
|
||||
public override int Version => 20220902;
|
||||
|
||||
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Configuration;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
@@ -30,13 +23,19 @@ using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Difficulty;
|
||||
using osu.Game.Rulesets.Mania.Edit;
|
||||
using osu.Game.Rulesets.Mania.Edit.Setup;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Legacy;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
@@ -47,7 +46,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
/// </summary>
|
||||
public const int MAX_STAGE_KEYS = 10;
|
||||
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableManiaRuleset(this, beatmap, mods);
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableManiaRuleset(this, beatmap, mods);
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
|
||||
|
||||
@@ -59,9 +58,20 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
public const string SHORT_NAME = "mania";
|
||||
|
||||
public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION;
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this);
|
||||
|
||||
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new ManiaLegacySkinTransformer(skin, beatmap);
|
||||
public override ISkin? CreateSkinTransformer(ISkin skin, IBeatmap beatmap)
|
||||
{
|
||||
switch (skin)
|
||||
{
|
||||
case LegacySkin:
|
||||
return new ManiaLegacySkinTransformer(skin, beatmap);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
|
||||
{
|
||||
@@ -282,7 +292,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame();
|
||||
|
||||
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new ManiaRulesetConfigManager(settings, RulesetInfo);
|
||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new ManiaRulesetConfigManager(settings, RulesetInfo);
|
||||
|
||||
public override RulesetSettingsSubsection CreateSettings() => new ManiaSettingsSubsection(this);
|
||||
|
||||
@@ -311,7 +321,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
return Array.Empty<KeyBinding>();
|
||||
}
|
||||
|
||||
public override string GetVariantName(int variant)
|
||||
public override LocalisableString GetVariantName(int variant)
|
||||
{
|
||||
switch (getPlayfieldType(variant))
|
||||
{
|
||||
@@ -356,7 +366,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
};
|
||||
}
|
||||
|
||||
public override string GetDisplayNameForHitResult(HitResult result)
|
||||
public override LocalisableString GetDisplayNameForHitResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
@@ -34,7 +33,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
LabelText = "Scrolling direction",
|
||||
Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
|
||||
},
|
||||
new SettingsSlider<double, TimeSlider>
|
||||
new SettingsSlider<double, ManiaScrollSlider>
|
||||
{
|
||||
LabelText = "Scroll speed",
|
||||
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
|
||||
@@ -47,5 +46,10 @@ namespace osu.Game.Rulesets.Mania
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private class ManiaScrollSlider : OsuSliderBar<double>
|
||||
{
|
||||
public override LocalisableString TooltipText => $"{Current.Value}ms (speed {(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / Current.Value)})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
@@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
public override double ScoreMultiplier => 0.9;
|
||||
|
||||
public override string Description => "No more tricky speed changes!";
|
||||
public override LocalisableString Description => "No more tricky speed changes!";
|
||||
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Equals;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public override string Name => "Dual Stages";
|
||||
public override string Acronym => "DS";
|
||||
public override string Description => @"Double the stages, double the fun!";
|
||||
public override LocalisableString Description => @"Double the stages, double the fun!";
|
||||
public override ModType Type => ModType.Conversion;
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModEasy : ModEasyWithExtraLives
|
||||
{
|
||||
public override string Description => @"More forgiving HP drain, less accuracy required, and three lives!";
|
||||
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and three lives!";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
@@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public override string Name => "Fade In";
|
||||
public override string Acronym => "FI";
|
||||
public override string Description => @"Keys appear out of nowhere!";
|
||||
public override LocalisableString Description => @"Keys appear out of nowhere!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray();
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModHidden : ManiaModPlayfieldCover
|
||||
{
|
||||
public override string Description => @"Keys fade out before you hit them!";
|
||||
public override LocalisableString Description => @"Keys fade out before you hit them!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray();
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
@@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override string Description => @"Replaces all hold notes with normal notes.";
|
||||
public override LocalisableString Description => @"Replaces all hold notes with normal notes.";
|
||||
|
||||
public override IconUsage? Icon => FontAwesome.Solid.DotCircle;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
@@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override string Acronym => "IN";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override string Description => "Hold the keys. To the beat.";
|
||||
public override LocalisableString Description => "Hold the keys. To the beat.";
|
||||
|
||||
public override IconUsage? Icon => FontAwesome.Solid.YinYang;
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey1 : ManiaKeyMod
|
||||
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 1;
|
||||
public override string Name => "One Key";
|
||||
public override string Acronym => "1K";
|
||||
public override string Description => @"Play with one key.";
|
||||
public override LocalisableString Description => @"Play with one key.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey10 : ManiaKeyMod
|
||||
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 10;
|
||||
public override string Name => "Ten Keys";
|
||||
public override string Acronym => "10K";
|
||||
public override string Description => @"Play with ten keys.";
|
||||
public override LocalisableString Description => @"Play with ten keys.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey2 : ManiaKeyMod
|
||||
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 2;
|
||||
public override string Name => "Two Keys";
|
||||
public override string Acronym => "2K";
|
||||
public override string Description => @"Play with two keys.";
|
||||
public override LocalisableString Description => @"Play with two keys.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey3 : ManiaKeyMod
|
||||
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 3;
|
||||
public override string Name => "Three Keys";
|
||||
public override string Acronym => "3K";
|
||||
public override string Description => @"Play with three keys.";
|
||||
public override LocalisableString Description => @"Play with three keys.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey4 : ManiaKeyMod
|
||||
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 4;
|
||||
public override string Name => "Four Keys";
|
||||
public override string Acronym => "4K";
|
||||
public override string Description => @"Play with four keys.";
|
||||
public override LocalisableString Description => @"Play with four keys.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey5 : ManiaKeyMod
|
||||
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 5;
|
||||
public override string Name => "Five Keys";
|
||||
public override string Acronym => "5K";
|
||||
public override string Description => @"Play with five keys.";
|
||||
public override LocalisableString Description => @"Play with five keys.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey6 : ManiaKeyMod
|
||||
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 6;
|
||||
public override string Name => "Six Keys";
|
||||
public override string Acronym => "6K";
|
||||
public override string Description => @"Play with six keys.";
|
||||
public override LocalisableString Description => @"Play with six keys.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey7 : ManiaKeyMod
|
||||
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 7;
|
||||
public override string Name => "Seven Keys";
|
||||
public override string Acronym => "7K";
|
||||
public override string Description => @"Play with seven keys.";
|
||||
public override LocalisableString Description => @"Play with seven keys.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey8 : ManiaKeyMod
|
||||
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 8;
|
||||
public override string Name => "Eight Keys";
|
||||
public override string Acronym => "8K";
|
||||
public override string Description => @"Play with eight keys.";
|
||||
public override LocalisableString Description => @"Play with eight keys.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey9 : ManiaKeyMod
|
||||
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override int KeyCount => 9;
|
||||
public override string Name => "Nine Keys";
|
||||
public override string Acronym => "9K";
|
||||
public override string Description => @"Play with nine keys.";
|
||||
public override LocalisableString Description => @"Play with nine keys.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using System.Linq;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
|
||||
@@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModMirror : ModMirror, IApplicableToBeatmap
|
||||
{
|
||||
public override string Description => "Notes are flipped horizontally.";
|
||||
public override LocalisableString Description => "Notes are flipped horizontally.";
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer;
|
||||
Container hocParent = (Container)hoc.Parent;
|
||||
|
||||
hocParent.Remove(hoc);
|
||||
hocParent.Remove(hoc, false);
|
||||
hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c =>
|
||||
{
|
||||
c.RelativeSizeAxes = Axes.Both;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
@@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModRandom : ModRandom, IApplicableToBeatmap
|
||||
{
|
||||
public override string Description => @"Shuffle around the keys!";
|
||||
public override LocalisableString Description => @"Shuffle around the keys!";
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
|
||||
@@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
else
|
||||
{
|
||||
lightContainer.FadeOut(120)
|
||||
.OnComplete(d => Column.TopLevelContainer.Remove(d));
|
||||
.OnComplete(d => Column.TopLevelContainer.Remove(d, false));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@@ -14,6 +16,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public class TestSceneObjectMerging : TestSceneOsuEditor
|
||||
{
|
||||
private OsuSelectionHandler selectionHandler => Editor.ChildrenOfType<OsuSelectionHandler>().First();
|
||||
|
||||
[Test]
|
||||
public void TestSimpleMerge()
|
||||
{
|
||||
@@ -29,6 +33,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
EditorBeatmap.SelectedHitObjects.Add(circle2);
|
||||
});
|
||||
|
||||
moveMouseToHitObject(1);
|
||||
AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection"));
|
||||
|
||||
mergeSelection();
|
||||
|
||||
AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor(
|
||||
@@ -77,6 +84,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
return sliderCreatedFor(args);
|
||||
});
|
||||
|
||||
AddAssert("samples exist", sliderSampleExist);
|
||||
|
||||
AddStep("undo", () => Editor.Undo());
|
||||
AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && slider is not null && objectsRestored(circle1, slider, circle2));
|
||||
}
|
||||
@@ -122,6 +131,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
return sliderCreatedFor(args);
|
||||
});
|
||||
|
||||
AddAssert("samples exist", sliderSampleExist);
|
||||
|
||||
AddAssert("merged slider matches first slider", () =>
|
||||
{
|
||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||
@@ -165,9 +176,62 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
(pos: circle1.Position, pathType: PathType.Linear),
|
||||
(pos: circle2.Position, pathType: null)));
|
||||
|
||||
AddAssert("samples exist", sliderSampleExist);
|
||||
|
||||
AddAssert("spinner not merged", () => EditorBeatmap.HitObjects.Contains(spinner));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestIllegalMerge()
|
||||
{
|
||||
HitCircle? circle1 = null;
|
||||
HitCircle? circle2 = null;
|
||||
|
||||
AddStep("add two circles on the same position", () =>
|
||||
{
|
||||
circle1 = new HitCircle();
|
||||
circle2 = new HitCircle { Position = circle1.Position + Vector2.UnitX };
|
||||
EditorClock.Seek(0);
|
||||
EditorBeatmap.Add(circle1);
|
||||
EditorBeatmap.Add(circle2);
|
||||
EditorBeatmap.SelectedHitObjects.Add(circle1);
|
||||
EditorBeatmap.SelectedHitObjects.Add(circle2);
|
||||
});
|
||||
|
||||
moveMouseToHitObject(1);
|
||||
AddAssert("merge option not available", () => selectionHandler.ContextMenuItems.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection"));
|
||||
mergeSelection();
|
||||
AddAssert("circles not merged", () => circle1 is not null && circle2 is not null
|
||||
&& EditorBeatmap.HitObjects.Contains(circle1) && EditorBeatmap.HitObjects.Contains(circle2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSameStartTimeMerge()
|
||||
{
|
||||
HitCircle? circle1 = null;
|
||||
HitCircle? circle2 = null;
|
||||
|
||||
AddStep("add two circles at the same time", () =>
|
||||
{
|
||||
circle1 = new HitCircle();
|
||||
circle2 = new HitCircle { Position = circle1.Position + 100 * Vector2.UnitX };
|
||||
EditorClock.Seek(0);
|
||||
EditorBeatmap.Add(circle1);
|
||||
EditorBeatmap.Add(circle2);
|
||||
EditorBeatmap.SelectedHitObjects.Add(circle1);
|
||||
EditorBeatmap.SelectedHitObjects.Add(circle2);
|
||||
});
|
||||
|
||||
moveMouseToHitObject(1);
|
||||
AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection"));
|
||||
|
||||
mergeSelection();
|
||||
|
||||
AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor(
|
||||
(pos: circle1.Position, pathType: PathType.Linear),
|
||||
(pos: circle2.Position, pathType: null)));
|
||||
}
|
||||
|
||||
private void mergeSelection()
|
||||
{
|
||||
AddStep("merge selection", () =>
|
||||
@@ -209,5 +273,27 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool sliderSampleExist()
|
||||
{
|
||||
if (EditorBeatmap.SelectedHitObjects.Count != 1)
|
||||
return false;
|
||||
|
||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||
|
||||
return mergedSlider.Samples[0] is not null;
|
||||
}
|
||||
|
||||
private void moveMouseToHitObject(int index)
|
||||
{
|
||||
AddStep($"hover mouse over hit object {index}", () =>
|
||||
{
|
||||
if (EditorBeatmap.HitObjects.Count <= index)
|
||||
return;
|
||||
|
||||
Vector2 position = ((OsuHitObject)EditorBeatmap.HitObjects[index]).Position;
|
||||
InputManager.MoveMouseTo(selectionHandler.ToScreenSpace(position));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,10 +59,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
}
|
||||
});
|
||||
|
||||
editorClock = new EditorClock(editorBeatmap);
|
||||
|
||||
base.Content.Children = new Drawable[]
|
||||
{
|
||||
editorClock = new EditorClock(editorBeatmap),
|
||||
snapProvider,
|
||||
Content
|
||||
};
|
||||
|
||||
@@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
AddStep($"click context menu item \"{contextMenuText}\"", () =>
|
||||
{
|
||||
MenuItem item = visualiser.ContextMenuItems[1].Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
|
||||
MenuItem item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == "Curve type")?.Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
|
||||
|
||||
item?.Action?.Value();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public class TestSceneSliderSplitting : EditorTestScene
|
||||
{
|
||||
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||
|
||||
private ComposeBlueprintContainer blueprintContainer
|
||||
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
|
||||
|
||||
private Slider? slider;
|
||||
private PathControlPointVisualiser? visualiser;
|
||||
|
||||
private const double split_gap = 100;
|
||||
|
||||
[Test]
|
||||
public void TestBasicSplit()
|
||||
{
|
||||
double endTime = 0;
|
||||
|
||||
AddStep("add slider", () =>
|
||||
{
|
||||
slider = new Slider
|
||||
{
|
||||
Position = new Vector2(0, 50),
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
|
||||
new PathControlPoint(new Vector2(150, 150)),
|
||||
new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
|
||||
new PathControlPoint(new Vector2(400, 0)),
|
||||
new PathControlPoint(new Vector2(400, 150))
|
||||
})
|
||||
};
|
||||
|
||||
EditorBeatmap.Add(slider);
|
||||
|
||||
endTime = slider.EndTime;
|
||||
});
|
||||
|
||||
AddStep("select added slider", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First();
|
||||
});
|
||||
|
||||
moveMouseToControlPoint(2);
|
||||
AddStep("select control point", () =>
|
||||
{
|
||||
if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true;
|
||||
});
|
||||
addContextMenuItemStep("Split control point");
|
||||
|
||||
AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 2 &&
|
||||
sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap,
|
||||
(new Vector2(0, 50), PathType.PerfectCurve),
|
||||
(new Vector2(150, 200), null),
|
||||
(new Vector2(300, 50), null)
|
||||
) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], slider.StartTime, endTime + split_gap,
|
||||
(new Vector2(300, 50), PathType.PerfectCurve),
|
||||
(new Vector2(400, 50), null),
|
||||
(new Vector2(400, 200), null)
|
||||
));
|
||||
|
||||
AddStep("undo", () => Editor.Undo());
|
||||
AddAssert("original slider restored", () => EditorBeatmap.HitObjects.Count == 1 && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, endTime,
|
||||
(new Vector2(0, 50), PathType.PerfectCurve),
|
||||
(new Vector2(150, 200), null),
|
||||
(new Vector2(300, 50), PathType.PerfectCurve),
|
||||
(new Vector2(400, 50), null),
|
||||
(new Vector2(400, 200), null)
|
||||
));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDoubleSplit()
|
||||
{
|
||||
double endTime = 0;
|
||||
|
||||
AddStep("add slider", () =>
|
||||
{
|
||||
slider = new Slider
|
||||
{
|
||||
Position = new Vector2(0, 50),
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
|
||||
new PathControlPoint(new Vector2(150, 150)),
|
||||
new PathControlPoint(new Vector2(300, 0), PathType.Bezier),
|
||||
new PathControlPoint(new Vector2(400, 0)),
|
||||
new PathControlPoint(new Vector2(400, 150), PathType.Catmull),
|
||||
new PathControlPoint(new Vector2(300, 200)),
|
||||
new PathControlPoint(new Vector2(400, 250))
|
||||
})
|
||||
};
|
||||
|
||||
EditorBeatmap.Add(slider);
|
||||
|
||||
endTime = slider.EndTime;
|
||||
});
|
||||
|
||||
AddStep("select added slider", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First();
|
||||
});
|
||||
|
||||
moveMouseToControlPoint(2);
|
||||
AddStep("select first control point", () =>
|
||||
{
|
||||
if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true;
|
||||
});
|
||||
moveMouseToControlPoint(4);
|
||||
AddStep("select second control point", () =>
|
||||
{
|
||||
if (visualiser is not null) visualiser.Pieces[4].IsSelected.Value = true;
|
||||
});
|
||||
addContextMenuItemStep("Split 2 control points");
|
||||
|
||||
AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 3 &&
|
||||
sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap,
|
||||
(new Vector2(0, 50), PathType.PerfectCurve),
|
||||
(new Vector2(150, 200), null),
|
||||
(new Vector2(300, 50), null)
|
||||
) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], EditorBeatmap.HitObjects[0].GetEndTime() + split_gap, slider.StartTime - split_gap,
|
||||
(new Vector2(300, 50), PathType.Bezier),
|
||||
(new Vector2(400, 50), null),
|
||||
(new Vector2(400, 200), null)
|
||||
) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[2], EditorBeatmap.HitObjects[1].GetEndTime() + split_gap, endTime + split_gap * 2,
|
||||
(new Vector2(400, 200), PathType.Catmull),
|
||||
(new Vector2(300, 250), null),
|
||||
(new Vector2(400, 300), null)
|
||||
));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSplitRetainsHitsounds()
|
||||
{
|
||||
HitSampleInfo? sample = null;
|
||||
|
||||
AddStep("add slider", () =>
|
||||
{
|
||||
slider = new Slider
|
||||
{
|
||||
Position = new Vector2(0, 50),
|
||||
LegacyLastTickOffset = 36, // This is necessary for undo to retain the sample control point
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
|
||||
new PathControlPoint(new Vector2(150, 150)),
|
||||
new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
|
||||
new PathControlPoint(new Vector2(400, 0)),
|
||||
new PathControlPoint(new Vector2(400, 150))
|
||||
})
|
||||
};
|
||||
|
||||
EditorBeatmap.Add(slider);
|
||||
});
|
||||
|
||||
AddStep("add hitsounds", () =>
|
||||
{
|
||||
if (slider is null) return;
|
||||
|
||||
slider.SampleControlPoint.SampleBank = "soft";
|
||||
slider.SampleControlPoint.SampleVolume = 70;
|
||||
sample = new HitSampleInfo("hitwhistle");
|
||||
slider.Samples.Add(sample);
|
||||
});
|
||||
|
||||
AddStep("select added slider", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First();
|
||||
});
|
||||
|
||||
moveMouseToControlPoint(2);
|
||||
AddStep("select control point", () =>
|
||||
{
|
||||
if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true;
|
||||
});
|
||||
addContextMenuItemStep("Split control point");
|
||||
AddAssert("sliders have hitsounds", hasHitsounds);
|
||||
|
||||
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0]));
|
||||
AddStep("remove first slider", () => EditorBeatmap.RemoveAt(0));
|
||||
AddStep("undo", () => Editor.Undo());
|
||||
AddAssert("sliders have hitsounds", hasHitsounds);
|
||||
|
||||
bool hasHitsounds() => sample is not null &&
|
||||
EditorBeatmap.HitObjects.All(o => o.SampleControlPoint.SampleBank == "soft" &&
|
||||
o.SampleControlPoint.SampleVolume == 70 &&
|
||||
o.Samples.Contains(sample));
|
||||
}
|
||||
|
||||
private bool sliderCreatedFor(Slider s, double startTime, double endTime, params (Vector2 pos, PathType? pathType)[] expectedControlPoints)
|
||||
{
|
||||
if (!Precision.AlmostEquals(s.StartTime, startTime, 1) || !Precision.AlmostEquals(s.EndTime, endTime, 1)) return false;
|
||||
|
||||
int i = 0;
|
||||
|
||||
foreach ((Vector2 pos, PathType? pathType) in expectedControlPoints)
|
||||
{
|
||||
var controlPoint = s.Path.ControlPoints[i++];
|
||||
|
||||
if (!Precision.AlmostEquals(controlPoint.Position + s.Position, pos) || controlPoint.Type != pathType)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void moveMouseToControlPoint(int index)
|
||||
{
|
||||
AddStep($"move mouse to control point {index}", () =>
|
||||
{
|
||||
if (slider is null || visualiser is null) return;
|
||||
|
||||
Vector2 position = slider.Path.ControlPoints[index].Position + slider.Position;
|
||||
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position));
|
||||
});
|
||||
}
|
||||
|
||||
private void addContextMenuItemStep(string contextMenuText)
|
||||
{
|
||||
AddStep($"click context menu item \"{contextMenuText}\"", () =>
|
||||
{
|
||||
if (visualiser is null) return;
|
||||
|
||||
MenuItem? item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
|
||||
|
||||
item?.Action?.Value();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
@@ -125,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
/// Ensures alternation is reset before the first hitobject after a break.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestInputSingularWithBreak() => CreateModTest(new ModTestData
|
||||
public void TestInputSingularWithBreak([Values] bool pressBeforeSecondObject) => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModAlternate(),
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2,
|
||||
@@ -155,21 +156,26 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
},
|
||||
}
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
ReplayFrames = new ReplayFrame[]
|
||||
{
|
||||
// first press to start alternate lock.
|
||||
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(501, new Vector2(100)),
|
||||
// press same key after break but before hit object.
|
||||
new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2251, new Vector2(300, 100)),
|
||||
new OsuReplayFrame(450, new Vector2(100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(451, new Vector2(100)),
|
||||
// press same key at second hitobject and ensure it has been hit.
|
||||
new OsuReplayFrame(2500, new Vector2(500, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2501, new Vector2(500, 100)),
|
||||
new OsuReplayFrame(2450, new Vector2(500, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2451, new Vector2(500, 100)),
|
||||
// press same key at third hitobject and ensure it has been missed.
|
||||
new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(3001, new Vector2(500, 100)),
|
||||
}
|
||||
new OsuReplayFrame(2950, new Vector2(500, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2951, new Vector2(500, 100)),
|
||||
}.Concat(!pressBeforeSecondObject
|
||||
? Enumerable.Empty<ReplayFrame>()
|
||||
: new ReplayFrame[]
|
||||
{
|
||||
// press same key after break but before hit object.
|
||||
new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2251, new Vector2(300, 100)),
|
||||
}
|
||||
).ToList()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
var drawableObject = getFunc.Invoke();
|
||||
|
||||
hitObjectContainer.Remove(drawableObject);
|
||||
hitObjectContainer.Remove(drawableObject, false);
|
||||
followPointRenderer.RemoveFollowPoints(drawableObject.HitObject);
|
||||
});
|
||||
}
|
||||
@@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
else
|
||||
targetTime = getObject(hitObjectContainer.Count - 1).HitObject.StartTime + 1;
|
||||
|
||||
hitObjectContainer.Remove(toReorder);
|
||||
hitObjectContainer.Remove(toReorder, false);
|
||||
toReorder.HitObject.StartTime = targetTime;
|
||||
hitObjectContainer.Add(toReorder);
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
|
||||
tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1";
|
||||
|
||||
var provider = Ruleset.Value.CreateInstance().CreateLegacySkinProvider(tintingSkin, Beatmap.Value.Beatmap);
|
||||
var provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(tintingSkin, Beatmap.Value.Beatmap);
|
||||
|
||||
Child = new SkinProvidingContainer(provider)
|
||||
{
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<PackageReference Include="Moq" Version="4.18.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
||||
@@ -18,11 +18,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
private const double min_velocity = 0.5;
|
||||
private const double slider_multiplier = 1.3;
|
||||
|
||||
private const double min_angle_multiplier = 0.2;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the difficulty of memorising and hitting an object, based on:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>distance between a number of previous objects and the current object,</description></item>
|
||||
/// <item><description>the visual opacity of the current object,</description></item>
|
||||
/// <item><description>the angle made by the current object,</description></item>
|
||||
/// <item><description>length and speed of the current object (for sliders),</description></item>
|
||||
/// <item><description>and whether the hidden mod is enabled.</description></item>
|
||||
/// </list>
|
||||
@@ -43,6 +46,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
|
||||
OsuDifficultyHitObject lastObj = osuCurrent;
|
||||
|
||||
double angleRepeatCount = 0.0;
|
||||
|
||||
// This is iterating backwards in time from the current object.
|
||||
for (int i = 0; i < Math.Min(current.Index, 10); i++)
|
||||
{
|
||||
@@ -66,6 +71,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
double opacityBonus = 1.0 + max_opacity_bonus * (1.0 - osuCurrent.OpacityAt(currentHitObject.StartTime, hidden));
|
||||
|
||||
result += stackNerf * opacityBonus * scalingFactor * jumpDistance / cumulativeStrainTime;
|
||||
|
||||
if (currentObj.Angle != null && osuCurrent.Angle != null)
|
||||
{
|
||||
// Objects further back in time should count less for the nerf.
|
||||
if (Math.Abs(currentObj.Angle.Value - osuCurrent.Angle.Value) < 0.02)
|
||||
angleRepeatCount += Math.Max(1.0 - 0.1 * i, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
lastObj = currentObj;
|
||||
@@ -77,6 +89,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
if (hidden)
|
||||
result *= 1.0 + hidden_bonus;
|
||||
|
||||
// Nerf patterns with repeated angles.
|
||||
result *= min_angle_multiplier + (1.0 - min_angle_multiplier) / (angleRepeatCount + 1.0);
|
||||
|
||||
double sliderBonus = 0.0;
|
||||
|
||||
if (osuCurrent.BaseObject is Slider osuSlider)
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
/// <summary>
|
||||
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, double greatWindow)
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||
{
|
||||
if (current.BaseObject is Spinner)
|
||||
return 0;
|
||||
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
double lastDelta = lastObj.StrainTime;
|
||||
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
|
||||
|
||||
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
|
||||
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - currObj.HitWindowGreat * 0.3) / (currObj.HitWindowGreat * 0.3));
|
||||
|
||||
windowPenalty = Math.Min(1, windowPenalty);
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
/// <item><description>and how easily they can be cheesed.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, double greatWindow)
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||
{
|
||||
if (current.BaseObject is Spinner)
|
||||
return 0;
|
||||
@@ -35,7 +35,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
var osuNextObj = (OsuDifficultyHitObject)current.Next(0);
|
||||
|
||||
double strainTime = osuCurrObj.StrainTime;
|
||||
double greatWindowFull = greatWindow * 2;
|
||||
double doubletapness = 1;
|
||||
|
||||
// Nerf doubletappable doubles.
|
||||
@@ -45,13 +44,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
double nextDeltaTime = Math.Max(1, osuNextObj.DeltaTime);
|
||||
double deltaDifference = Math.Abs(nextDeltaTime - currDeltaTime);
|
||||
double speedRatio = currDeltaTime / Math.Max(currDeltaTime, deltaDifference);
|
||||
double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / greatWindowFull), 2);
|
||||
double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / osuCurrObj.HitWindowGreat), 2);
|
||||
doubletapness = Math.Pow(speedRatio, 1 - windowRatio);
|
||||
}
|
||||
|
||||
// Cap deltatime to the OD 300 hitwindow.
|
||||
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
||||
strainTime /= Math.Clamp((strainTime / greatWindowFull) / 0.93, 0.92, 1);
|
||||
strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindowGreat) / 0.93, 0.92, 1);
|
||||
|
||||
// derive speedBonus for calculation
|
||||
double speedBonus = 1.0;
|
||||
|
||||
@@ -23,9 +23,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
public class OsuDifficultyCalculator : DifficultyCalculator
|
||||
{
|
||||
private const double difficulty_multiplier = 0.0675;
|
||||
private double hitWindowGreat;
|
||||
|
||||
public override int Version => 20220701;
|
||||
public override int Version => 20220902;
|
||||
|
||||
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
@@ -76,6 +75,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
||||
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
|
||||
|
||||
HitWindows hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
|
||||
return new OsuDifficultyAttributes
|
||||
{
|
||||
StarRating = starRating,
|
||||
@@ -112,16 +116,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
{
|
||||
HitWindows hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
|
||||
return new Skill[]
|
||||
{
|
||||
new Aim(mods, true),
|
||||
new Aim(mods, false),
|
||||
new Speed(mods, hitWindowGreat),
|
||||
new Speed(mods),
|
||||
new Flashlight(mods)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
@@ -78,6 +79,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
/// </summary>
|
||||
public double? Angle { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the full hit window for a Great <see cref="HitResult"/>.
|
||||
/// </summary>
|
||||
public double HitWindowGreat { get; private set; }
|
||||
|
||||
private readonly OsuHitObject lastLastObject;
|
||||
private readonly OsuHitObject lastObject;
|
||||
|
||||
@@ -90,6 +96,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
||||
StrainTime = Math.Max(DeltaTime, min_delta_time);
|
||||
|
||||
if (BaseObject is Slider sliderObject)
|
||||
{
|
||||
HitWindowGreat = 2 * sliderObject.HeadCircle.HitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
}
|
||||
else
|
||||
{
|
||||
HitWindowGreat = 2 * BaseObject.HitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
}
|
||||
|
||||
setDistances(clockRate);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
hasHiddenMod = mods.Any(m => m is OsuModHidden);
|
||||
}
|
||||
|
||||
private double skillMultiplier => 0.05;
|
||||
private double skillMultiplier => 0.052;
|
||||
private double strainDecayBase => 0.15;
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
@@ -26,14 +26,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
|
||||
protected override int ReducedSectionCount => 5;
|
||||
protected override double DifficultyMultiplier => 1.04;
|
||||
private readonly double greatWindow;
|
||||
|
||||
private readonly List<double> objectStrains = new List<double>();
|
||||
|
||||
public Speed(Mod[] mods, double hitWindowGreat)
|
||||
public Speed(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
greatWindow = hitWindowGreat;
|
||||
}
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
@@ -43,9 +41,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
protected override double StrainValueAt(DifficultyHitObject current)
|
||||
{
|
||||
currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime);
|
||||
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, greatWindow) * skillMultiplier;
|
||||
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier;
|
||||
|
||||
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current, greatWindow);
|
||||
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
|
||||
|
||||
double totalStrain = currentStrain * currentRhythm;
|
||||
|
||||
@@ -64,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
if (maxStrain == 0)
|
||||
return 0;
|
||||
|
||||
return objectStrains.Aggregate((total, next) => total + (1.0 / (1.0 + Math.Exp(-(next / maxStrain * 12.0 - 6.0)))));
|
||||
return objectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@@ -53,6 +54,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
private IBindable<Vector2> sliderPosition;
|
||||
private IBindable<float> sliderScale;
|
||||
|
||||
[UsedImplicitly]
|
||||
private readonly IBindable<int> sliderVersion;
|
||||
|
||||
public PathControlPointPiece(Slider slider, PathControlPoint controlPoint)
|
||||
{
|
||||
this.slider = slider;
|
||||
@@ -61,11 +65,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
// we don't want to run the path type update on construction as it may inadvertently change the slider.
|
||||
cachePoints(slider);
|
||||
|
||||
slider.Path.Version.BindValueChanged(_ =>
|
||||
sliderVersion = slider.Path.Version.GetBoundCopy();
|
||||
|
||||
// schedule ensure that updates are only applied after all operations from a single frame are applied.
|
||||
// this avoids inadvertently changing the slider path type for batch operations.
|
||||
sliderVersion.BindValueChanged(_ => Scheduler.AddOnce(() =>
|
||||
{
|
||||
cachePoints(slider);
|
||||
updatePathType();
|
||||
});
|
||||
}));
|
||||
|
||||
controlPoint.Changed += updateMarkerDisplay;
|
||||
|
||||
|
||||
+54
-11
@@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
private InputManager inputManager;
|
||||
|
||||
public Action<List<PathControlPoint>> RemoveControlPointsRequested;
|
||||
public Action<List<PathControlPoint>> SplitControlPointsRequested;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider snapProvider { get; set; }
|
||||
@@ -104,6 +105,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool splitSelected()
|
||||
{
|
||||
List<PathControlPoint> controlPointsToSplitAt = Pieces.Where(p => p.IsSelected.Value && isSplittable(p)).Select(p => p.ControlPoint).ToList();
|
||||
|
||||
// Ensure that there are any points to be split
|
||||
if (controlPointsToSplitAt.Count == 0)
|
||||
return false;
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
SplitControlPointsRequested?.Invoke(controlPointsToSplitAt);
|
||||
changeHandler?.EndChange();
|
||||
|
||||
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
|
||||
foreach (var piece in Pieces)
|
||||
piece.IsSelected.Value = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool isSplittable(PathControlPointPiece p) =>
|
||||
// A slider can only be split on control points which connect two different slider segments.
|
||||
p.ControlPoint.Type.HasValue && p != Pieces.FirstOrDefault() && p != Pieces.LastOrDefault();
|
||||
|
||||
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
switch (e.Action)
|
||||
@@ -142,8 +166,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
foreach (var point in e.OldItems.Cast<PathControlPoint>())
|
||||
{
|
||||
Pieces.RemoveAll(p => p.ControlPoint == point);
|
||||
Connections.RemoveAll(c => c.ControlPoint == point);
|
||||
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
|
||||
piece.RemoveAndDisposeImmediately();
|
||||
foreach (var connection in Connections.Where(c => c.ControlPoint == point).ToArray())
|
||||
connection.RemoveAndDisposeImmediately();
|
||||
}
|
||||
|
||||
// If removing before the end of the path,
|
||||
@@ -322,25 +348,42 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
if (count == 0)
|
||||
return null;
|
||||
|
||||
List<MenuItem> items = new List<MenuItem>();
|
||||
var splittablePieces = selectedPieces.Where(isSplittable).ToList();
|
||||
int splittableCount = splittablePieces.Count;
|
||||
|
||||
List<MenuItem> curveTypeItems = new List<MenuItem>();
|
||||
|
||||
if (!selectedPieces.Contains(Pieces[0]))
|
||||
items.Add(createMenuItemForPathType(null));
|
||||
curveTypeItems.Add(createMenuItemForPathType(null));
|
||||
|
||||
// todo: hide/disable items which aren't valid for selected points
|
||||
items.Add(createMenuItemForPathType(PathType.Linear));
|
||||
items.Add(createMenuItemForPathType(PathType.PerfectCurve));
|
||||
items.Add(createMenuItemForPathType(PathType.Bezier));
|
||||
items.Add(createMenuItemForPathType(PathType.Catmull));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.Linear));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.PerfectCurve));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.Bezier));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.Catmull));
|
||||
|
||||
return new MenuItem[]
|
||||
var menuItems = new List<MenuItem>
|
||||
{
|
||||
new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, () => DeleteSelected()),
|
||||
new OsuMenuItem("Curve type")
|
||||
{
|
||||
Items = items
|
||||
Items = curveTypeItems
|
||||
}
|
||||
};
|
||||
|
||||
if (splittableCount > 0)
|
||||
{
|
||||
menuItems.Add(new OsuMenuItem($"Split {"control point".ToQuantity(splittableCount, splittableCount > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}",
|
||||
MenuItemType.Destructive,
|
||||
() => splitSelected()));
|
||||
}
|
||||
|
||||
menuItems.Add(
|
||||
new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}",
|
||||
MenuItemType.Destructive,
|
||||
() => DeleteSelected())
|
||||
);
|
||||
|
||||
return menuItems.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint();
|
||||
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
|
||||
|
||||
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.
|
||||
// Without re-applying defaults, velocity won't be updated.
|
||||
ApplyDefaultsToHitObject();
|
||||
break;
|
||||
|
||||
case SliderPlacementState.Body:
|
||||
|
||||
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@@ -111,7 +112,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(HitObject, true)
|
||||
{
|
||||
RemoveControlPointsRequested = removeControlPoints
|
||||
RemoveControlPointsRequested = removeControlPoints,
|
||||
SplitControlPointsRequested = splitControlPoints
|
||||
});
|
||||
|
||||
base.OnSelected();
|
||||
@@ -249,6 +251,74 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
HitObject.Position += first;
|
||||
}
|
||||
|
||||
private void splitControlPoints(List<PathControlPoint> controlPointsToSplitAt)
|
||||
{
|
||||
// Arbitrary gap in milliseconds to put between split slider pieces
|
||||
const double split_gap = 100;
|
||||
|
||||
// Ensure that there are any points to be split
|
||||
if (controlPointsToSplitAt.Count == 0)
|
||||
return;
|
||||
|
||||
editorBeatmap.SelectedHitObjects.Clear();
|
||||
|
||||
foreach (var splitPoint in controlPointsToSplitAt)
|
||||
{
|
||||
if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type is null)
|
||||
continue;
|
||||
|
||||
// Split off the section of slider before this control point so the remaining control points to split are in the latter part of the slider.
|
||||
int index = controlPoints.IndexOf(splitPoint);
|
||||
|
||||
if (index <= 0)
|
||||
continue;
|
||||
|
||||
// Extract the split portion and remove from the original slider.
|
||||
var splitControlPoints = controlPoints.Take(index + 1).ToList();
|
||||
controlPoints.RemoveRange(0, index);
|
||||
|
||||
// Turn the control points which were split off into a new slider.
|
||||
var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone();
|
||||
var difficultyPoint = (DifficultyControlPoint)HitObject.DifficultyControlPoint.DeepClone();
|
||||
|
||||
var newSlider = new Slider
|
||||
{
|
||||
StartTime = HitObject.StartTime,
|
||||
Position = HitObject.Position + splitControlPoints[0].Position,
|
||||
NewCombo = HitObject.NewCombo,
|
||||
SampleControlPoint = samplePoint,
|
||||
DifficultyControlPoint = difficultyPoint,
|
||||
LegacyLastTickOffset = HitObject.LegacyLastTickOffset,
|
||||
Samples = HitObject.Samples.Select(s => s.With()).ToList(),
|
||||
RepeatCount = HitObject.RepeatCount,
|
||||
NodeSamples = HitObject.NodeSamples.Select(n => (IList<HitSampleInfo>)n.Select(s => s.With()).ToList()).ToList(),
|
||||
Path = new SliderPath(splitControlPoints.Select(o => new PathControlPoint(o.Position - splitControlPoints[0].Position, o == splitControlPoints[^1] ? null : o.Type)).ToArray())
|
||||
};
|
||||
|
||||
// Increase the start time of the slider before adding the new slider so the new slider is immediately inserted at the correct index and internal state remains valid.
|
||||
HitObject.StartTime += split_gap;
|
||||
|
||||
editorBeatmap.Add(newSlider);
|
||||
|
||||
HitObject.NewCombo = false;
|
||||
HitObject.Path.ExpectedDistance.Value -= newSlider.Path.CalculatedDistance;
|
||||
HitObject.StartTime += newSlider.SpanDuration;
|
||||
|
||||
// In case the remainder of the slider has no length left over, give it length anyways so we don't get a 0 length slider.
|
||||
if (HitObject.Path.ExpectedDistance.Value <= Precision.DOUBLE_EPSILON)
|
||||
{
|
||||
HitObject.Path.ExpectedDistance.Value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Once all required pieces have been split off, the original slider has the final split.
|
||||
// As a final step, we must reset its control points to have an origin of (0,0).
|
||||
Vector2 first = controlPoints[0].Position;
|
||||
foreach (var c in controlPoints)
|
||||
c.Position -= first;
|
||||
HitObject.Position += first;
|
||||
}
|
||||
|
||||
private void convertToStream()
|
||||
{
|
||||
if (editorBeatmap == null || beatDivisor == null)
|
||||
|
||||
@@ -127,16 +127,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
didFlip = true;
|
||||
|
||||
var controlPoints = slider.Path.ControlPoints.Select(p =>
|
||||
new PathControlPoint(new Vector2(
|
||||
(direction == Direction.Horizontal ? -1 : 1) * p.Position.X,
|
||||
(direction == Direction.Vertical ? -1 : 1) * p.Position.Y
|
||||
), p.Type)).ToArray();
|
||||
|
||||
// Importantly, update as a single operation so automatic adjustment of control points to different
|
||||
// curve types does not unexpectedly trigger and change the slider's shape.
|
||||
slider.Path.ControlPoints.Clear();
|
||||
slider.Path.ControlPoints.AddRange(controlPoints);
|
||||
foreach (var cp in slider.Path.ControlPoints)
|
||||
{
|
||||
cp.Position = new Vector2(
|
||||
(direction == Direction.Horizontal ? -1 : 1) * cp.Position.X,
|
||||
(direction == Direction.Vertical ? -1 : 1) * cp.Position.Y
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,13 +183,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
if (h is IHasPath path)
|
||||
{
|
||||
var controlPoints = path.Path.ControlPoints.Select(p =>
|
||||
new PathControlPoint(RotatePointAroundOrigin(p.Position, Vector2.Zero, delta), p.Type)).ToArray();
|
||||
|
||||
// Importantly, update as a single operation so automatic adjustment of control points to different
|
||||
// curve types does not unexpectedly trigger and change the slider's shape.
|
||||
path.Path.ControlPoints.Clear();
|
||||
path.Path.ControlPoints.AddRange(controlPoints);
|
||||
foreach (PathControlPoint cp in path.Path.ControlPoints)
|
||||
cp.Position = RotatePointAroundOrigin(cp.Position, Vector2.Zero, delta);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,10 +350,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
var mergeableObjects = selectedMergeableObjects;
|
||||
|
||||
if (mergeableObjects.Length < 2)
|
||||
if (!canMerge(mergeableObjects))
|
||||
return;
|
||||
|
||||
ChangeHandler?.BeginChange();
|
||||
EditorBeatmap.BeginChange();
|
||||
|
||||
// Have an initial slider object.
|
||||
var firstHitObject = mergeableObjects[0];
|
||||
@@ -371,6 +363,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
Position = firstHitObject.Position,
|
||||
NewCombo = firstHitObject.NewCombo,
|
||||
SampleControlPoint = firstHitObject.SampleControlPoint,
|
||||
Samples = firstHitObject.Samples,
|
||||
};
|
||||
|
||||
if (mergedHitObject.Path.ControlPoints.Count == 0)
|
||||
@@ -436,7 +429,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
SelectedItems.Clear();
|
||||
SelectedItems.Add(mergedHitObject);
|
||||
|
||||
ChangeHandler?.EndChange();
|
||||
EditorBeatmap.EndChange();
|
||||
}
|
||||
|
||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
|
||||
@@ -444,8 +437,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
||||
yield return item;
|
||||
|
||||
if (selectedMergeableObjects.Length > 1)
|
||||
if (canMerge(selectedMergeableObjects))
|
||||
yield return new OsuMenuItem("Merge selection", MenuItemType.Destructive, mergeSelection);
|
||||
}
|
||||
|
||||
private bool canMerge(IReadOnlyList<OsuHitObject> objects) =>
|
||||
objects.Count > 1
|
||||
&& (objects.Any(h => h is Slider)
|
||||
|| objects.Zip(objects.Skip(1), (h1, h2) => Precision.DefinitelyBigger(Vector2.DistanceSquared(h1.Position, h2.Position), 1)).Any(x => x));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset<OsuHitObject>
|
||||
public abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IUpdatableByPlayfield
|
||||
{
|
||||
public override double ScoreMultiplier => 1.0;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(OsuModCinema) };
|
||||
@@ -62,15 +62,18 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
gameplayClock = drawableRuleset.FrameStableClock;
|
||||
}
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
if (LastAcceptedAction != null && nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
|
||||
LastAcceptedAction = null;
|
||||
}
|
||||
|
||||
protected abstract bool CheckValidNewAction(OsuAction action);
|
||||
|
||||
private bool checkCorrectAction(OsuAction action)
|
||||
{
|
||||
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
|
||||
{
|
||||
LastAcceptedAction = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (action)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
@@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public override string Name => @"Alternate";
|
||||
public override string Acronym => @"AL";
|
||||
public override string Description => @"Don't use the same key twice in a row!";
|
||||
public override LocalisableString Description => @"Don't use the same key twice in a row!";
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public override string Name => "Approach Different";
|
||||
public override string Acronym => "AD";
|
||||
public override string Description => "Never trust the approach circles...";
|
||||
public override LocalisableString Description => "Never trust the approach circles...";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.StateChanges;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override string Acronym => "AP";
|
||||
public override IconUsage? Icon => OsuIcon.ModAutopilot;
|
||||
public override ModType Type => ModType.Automation;
|
||||
public override string Description => @"Automatic cursor movement - just follow the rhythm.";
|
||||
public override LocalisableString Description => @"Automatic cursor movement - just follow the rhythm.";
|
||||
public override double ScoreMultiplier => 0.1;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) };
|
||||
|
||||
@@ -30,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
private OsuInputManager inputManager = null!;
|
||||
|
||||
private IFrameStableClock gameplayClock = null!;
|
||||
|
||||
private List<OsuReplayFrame> replayFrames = null!;
|
||||
|
||||
private int currentFrame;
|
||||
@@ -40,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
if (currentFrame == replayFrames.Count - 1) return;
|
||||
|
||||
double time = gameplayClock.CurrentTime;
|
||||
double time = playfield.Clock.CurrentTime;
|
||||
|
||||
// Very naive implementation of autopilot based on proximity to replay frames.
|
||||
// TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered).
|
||||
@@ -55,8 +54,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
gameplayClock = drawableRuleset.FrameStableClock;
|
||||
|
||||
// Grab the input manager to disable the user's cursor, and for future use
|
||||
inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
|
||||
inputManager.AllowUserCursorMovement = false;
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public class OsuModBlinds : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToHealthProcessor
|
||||
{
|
||||
public override string Name => "Blinds";
|
||||
public override string Description => "Play with blinds on your screen.";
|
||||
public override LocalisableString Description => "Play with blinds on your screen.";
|
||||
public override string Acronym => "BL";
|
||||
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Adjust;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
@@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public override IconUsage? Icon => FontAwesome.Solid.CompressArrowsAlt;
|
||||
|
||||
public override string Description => "Hit them at the right size!";
|
||||
public override LocalisableString Description => "Hit them at the right size!";
|
||||
|
||||
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
|
||||
public override BindableNumber<float> StartScale { get; } = new BindableFloat
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModEasy : ModEasyWithExtraLives
|
||||
{
|
||||
public override string Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!";
|
||||
public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
@@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public override IconUsage? Icon => FontAwesome.Solid.ArrowsAltV;
|
||||
|
||||
public override string Description => "Hit them at the right size!";
|
||||
public override LocalisableString Description => "Hit them at the right size!";
|
||||
|
||||
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
|
||||
public override BindableNumber<float> StartScale { get; } = new BindableFloat
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
[SettingSource("Only fade approach circles", "The main object body will not fade when enabled.")]
|
||||
public Bindable<bool> OnlyFadeApproachCircles { get; } = new BindableBool();
|
||||
|
||||
public override string Description => @"Play with no approach circles and fading circles/sliders.";
|
||||
public override LocalisableString Description => @"Play with no approach circles and fading circles/sliders.";
|
||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -22,12 +24,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override string Acronym => "MG";
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Magnet;
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override string Description => "No need to chase the circles – your cursor is a magnet!";
|
||||
public override LocalisableString Description => "No need to chase the circles – your cursor is a magnet!";
|
||||
public override double ScoreMultiplier => 0.5;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) };
|
||||
|
||||
private IFrameStableClock gameplayClock = null!;
|
||||
|
||||
[SettingSource("Attraction strength", "How strong the pull is.", 0)]
|
||||
public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)
|
||||
{
|
||||
@@ -38,8 +38,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
gameplayClock = drawableRuleset.FrameStableClock;
|
||||
|
||||
// Hide judgment displays and follow points as they won't make any sense.
|
||||
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
|
||||
drawableRuleset.Playfield.DisplayJudgements.Value = false;
|
||||
@@ -55,27 +53,27 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
switch (drawable)
|
||||
{
|
||||
case DrawableHitCircle circle:
|
||||
easeTo(circle, cursorPos);
|
||||
easeTo(playfield.Clock, circle, cursorPos);
|
||||
break;
|
||||
|
||||
case DrawableSlider slider:
|
||||
|
||||
if (!slider.HeadCircle.Result.HasResult)
|
||||
easeTo(slider, cursorPos);
|
||||
easeTo(playfield.Clock, slider, cursorPos);
|
||||
else
|
||||
easeTo(slider, cursorPos - slider.Ball.DrawPosition);
|
||||
easeTo(playfield.Clock, slider, cursorPos - slider.Ball.DrawPosition);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void easeTo(DrawableHitObject hitObject, Vector2 destination)
|
||||
private void easeTo(IFrameBasedClock clock, DrawableHitObject hitObject, Vector2 destination)
|
||||
{
|
||||
double dampLength = Interpolation.Lerp(3000, 40, AttractionStrength.Value);
|
||||
|
||||
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
|
||||
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);
|
||||
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, clock.ElapsedFrameTime);
|
||||
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, clock.ElapsedFrameTime);
|
||||
|
||||
hitObject.Position = new Vector2(x, y);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModMirror : ModMirror, IApplicableToHitObject
|
||||
{
|
||||
public override string Description => "Flip objects on the chosen axes.";
|
||||
public override LocalisableString Description => "Flip objects on the chosen axes.";
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) };
|
||||
|
||||
[SettingSource("Mirrored axes", "Choose which axes objects are mirrored over.")]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModNoScope : ModNoScope, IUpdatableByPlayfield, IApplicableToBeatmap
|
||||
{
|
||||
public override string Description => "Where's the cursor?";
|
||||
public override LocalisableString Description => "Where's the cursor?";
|
||||
|
||||
private PeriodTracker spinnerPeriods = null!;
|
||||
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Osu.Utils;
|
||||
|
||||
@@ -18,46 +21,106 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
/// </summary>
|
||||
public class OsuModRandom : ModRandom, IApplicableToBeatmap
|
||||
{
|
||||
public override string Description => "It never gets boring!";
|
||||
public override LocalisableString Description => "It never gets boring!";
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray();
|
||||
|
||||
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
|
||||
|
||||
private Random? rng;
|
||||
private Random random = null!;
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
if (!(beatmap is OsuBeatmap osuBeatmap))
|
||||
if (beatmap is not OsuBeatmap osuBeatmap)
|
||||
return;
|
||||
|
||||
Seed.Value ??= RNG.Next();
|
||||
|
||||
rng = new Random((int)Seed.Value);
|
||||
random = new Random((int)Seed.Value);
|
||||
|
||||
var positionInfos = OsuHitObjectGenerationUtils.GeneratePositionInfos(osuBeatmap.HitObjects);
|
||||
|
||||
float rateOfChangeMultiplier = 0;
|
||||
// Offsets the angles of all hit objects in a "section" by the same amount.
|
||||
float sectionOffset = 0;
|
||||
|
||||
foreach (var positionInfo in positionInfos)
|
||||
// Whether the angles are positive or negative (clockwise or counter-clockwise flow).
|
||||
bool flowDirection = false;
|
||||
|
||||
for (int i = 0; i < positionInfos.Count; i++)
|
||||
{
|
||||
// rateOfChangeMultiplier only changes every 5 iterations in a combo
|
||||
// to prevent shaky-line-shaped streams
|
||||
if (positionInfo.HitObject.IndexInCurrentCombo % 5 == 0)
|
||||
rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1;
|
||||
|
||||
if (positionInfo == positionInfos.First())
|
||||
if (shouldStartNewSection(osuBeatmap, positionInfos, i))
|
||||
{
|
||||
positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2);
|
||||
positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
|
||||
sectionOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.0008f);
|
||||
flowDirection = !flowDirection;
|
||||
}
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
positionInfos[i].DistanceFromPrevious = (float)(random.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2);
|
||||
positionInfos[i].RelativeAngle = (float)(random.NextDouble() * 2 * Math.PI - Math.PI);
|
||||
}
|
||||
else
|
||||
{
|
||||
positionInfo.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, positionInfo.DistanceFromPrevious / (playfield_diagonal * 0.5f));
|
||||
// Offsets only the angle of the current hit object if a flow change occurs.
|
||||
float flowChangeOffset = 0;
|
||||
|
||||
// Offsets only the angle of the current hit object.
|
||||
float oneTimeOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.002f);
|
||||
|
||||
if (shouldApplyFlowChange(positionInfos, i))
|
||||
{
|
||||
flowChangeOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.002f);
|
||||
flowDirection = !flowDirection;
|
||||
}
|
||||
|
||||
float totalOffset =
|
||||
// sectionOffset and oneTimeOffset should mainly affect patterns with large spacing.
|
||||
(sectionOffset + oneTimeOffset) * positionInfos[i].DistanceFromPrevious +
|
||||
// flowChangeOffset should mainly affect streams.
|
||||
flowChangeOffset * (playfield_diagonal - positionInfos[i].DistanceFromPrevious);
|
||||
|
||||
positionInfos[i].RelativeAngle = getRelativeTargetAngle(positionInfos[i].DistanceFromPrevious, totalOffset, flowDirection);
|
||||
}
|
||||
}
|
||||
|
||||
osuBeatmap.HitObjects = OsuHitObjectGenerationUtils.RepositionHitObjects(positionInfos);
|
||||
}
|
||||
|
||||
/// <param name="targetDistance">The target distance between the previous and the current <see cref="OsuHitObject"/>.</param>
|
||||
/// <param name="offset">The angle (in rad) by which the target angle should be offset.</param>
|
||||
/// <param name="flowDirection">Whether the relative angle should be positive or negative.</param>
|
||||
private static float getRelativeTargetAngle(float targetDistance, float offset, bool flowDirection)
|
||||
{
|
||||
float angle = (float)(2.16 / (1 + 200 * Math.Exp(0.036 * (targetDistance - 310))) + 0.5 + offset);
|
||||
float relativeAngle = (float)Math.PI - angle;
|
||||
return flowDirection ? -relativeAngle : relativeAngle;
|
||||
}
|
||||
|
||||
/// <returns>Whether a new section should be started at the current <see cref="OsuHitObject"/>.</returns>
|
||||
private bool shouldStartNewSection(OsuBeatmap beatmap, IReadOnlyList<OsuHitObjectGenerationUtils.ObjectPositionInfo> positionInfos, int i)
|
||||
{
|
||||
if (i == 0)
|
||||
return true;
|
||||
|
||||
// Exclude new-combo-spam and 1-2-combos.
|
||||
bool previousObjectStartedCombo = positionInfos[Math.Max(0, i - 2)].HitObject.IndexInCurrentCombo > 1 &&
|
||||
positionInfos[i - 1].HitObject.NewCombo;
|
||||
bool previousObjectWasOnDownbeat = OsuHitObjectGenerationUtils.IsHitObjectOnBeat(beatmap, positionInfos[i - 1].HitObject, true);
|
||||
bool previousObjectWasOnBeat = OsuHitObjectGenerationUtils.IsHitObjectOnBeat(beatmap, positionInfos[i - 1].HitObject);
|
||||
|
||||
return (previousObjectStartedCombo && random.NextDouble() < 0.6f) ||
|
||||
previousObjectWasOnDownbeat ||
|
||||
(previousObjectWasOnBeat && random.NextDouble() < 0.4f);
|
||||
}
|
||||
|
||||
/// <returns>Whether a flow change should be applied at the current <see cref="OsuHitObject"/>.</returns>
|
||||
private bool shouldApplyFlowChange(IReadOnlyList<OsuHitObjectGenerationUtils.ObjectPositionInfo> positionInfos, int i)
|
||||
{
|
||||
// Exclude new-combo-spam and 1-2-combos.
|
||||
bool previousObjectStartedCombo = positionInfos[Math.Max(0, i - 2)].HitObject.IndexInCurrentCombo > 1 &&
|
||||
positionInfos[i - 1].HitObject.NewCombo;
|
||||
|
||||
return previousObjectStartedCombo && random.NextDouble() < 0.6f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
|
||||
{
|
||||
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
|
||||
public override LocalisableString Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -22,12 +23,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override string Name => "Repel";
|
||||
public override string Acronym => "RP";
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override string Description => "Hit objects run away!";
|
||||
public override LocalisableString Description => "Hit objects run away!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised) };
|
||||
|
||||
private IFrameStableClock? gameplayClock;
|
||||
|
||||
[SettingSource("Repulsion strength", "How strong the repulsion is.", 0)]
|
||||
public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f)
|
||||
{
|
||||
@@ -38,8 +37,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
gameplayClock = drawableRuleset.FrameStableClock;
|
||||
|
||||
// Hide judgment displays and follow points as they won't make any sense.
|
||||
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
|
||||
drawableRuleset.Playfield.DisplayJudgements.Value = false;
|
||||
@@ -68,29 +65,27 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
switch (drawable)
|
||||
{
|
||||
case DrawableHitCircle circle:
|
||||
easeTo(circle, destination, cursorPos);
|
||||
easeTo(playfield.Clock, circle, destination, cursorPos);
|
||||
break;
|
||||
|
||||
case DrawableSlider slider:
|
||||
|
||||
if (!slider.HeadCircle.Result.HasResult)
|
||||
easeTo(slider, destination, cursorPos);
|
||||
easeTo(playfield.Clock, slider, destination, cursorPos);
|
||||
else
|
||||
easeTo(slider, destination - slider.Ball.DrawPosition, cursorPos);
|
||||
easeTo(playfield.Clock, slider, destination - slider.Ball.DrawPosition, cursorPos);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void easeTo(DrawableHitObject hitObject, Vector2 destination, Vector2 cursorPos)
|
||||
private void easeTo(IFrameBasedClock clock, DrawableHitObject hitObject, Vector2 destination, Vector2 cursorPos)
|
||||
{
|
||||
Debug.Assert(gameplayClock != null);
|
||||
|
||||
double dampLength = Vector2.Distance(hitObject.Position, cursorPos) / (0.04 * RepulsionStrength.Value + 0.04);
|
||||
|
||||
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
|
||||
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);
|
||||
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, clock.ElapsedFrameTime);
|
||||
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, clock.ElapsedFrameTime);
|
||||
|
||||
hitObject.Position = new Vector2(x, y);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
@@ -10,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public override string Name => @"Single Tap";
|
||||
public override string Acronym => @"SG";
|
||||
public override string Description => @"You must only use one key!";
|
||||
public override LocalisableString Description => @"You must only use one key!";
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray();
|
||||
|
||||
protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction == null || LastAcceptedAction == action;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override string Acronym => "SI";
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Undo;
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override string Description => "Circles spin in. No approach circles.";
|
||||
public override LocalisableString Description => "Circles spin in. No approach circles.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
// todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override string Acronym => "SO";
|
||||
public override IconUsage? Icon => OsuIcon.ModSpunOut;
|
||||
public override ModType Type => ModType.Automation;
|
||||
public override string Description => @"Spinners will be automatically completed.";
|
||||
public override LocalisableString Description => @"Spinners will be automatically completed.";
|
||||
public override double ScoreMultiplier => 0.9;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTarget) };
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override string Name => @"Strict Tracking";
|
||||
public override string Acronym => @"ST";
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override string Description => @"Once you start a slider, follow precisely or get a miss.";
|
||||
public override LocalisableString Description => @"Once you start a slider, follow precisely or get a miss.";
|
||||
public override double ScoreMultiplier => 1.0;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTarget) };
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user