1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-21 05:49:54 +08:00

Compare commits

..

615 Commits

357 changed files with 6960 additions and 16243 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2022.1.1",
"version": "2022.2.3",
"commands": [
"jb"
]
+6
View File
@@ -1,5 +1,11 @@
on: [push, pull_request]
name: Continuous Integration
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
inspect-code:
+4
View File
@@ -8,8 +8,12 @@ on:
workflows: ["Continuous Integration"]
types:
- completed
permissions: {}
jobs:
annotate:
permissions:
checks: write # to create checks (dorny/test-reporter)
name: Annotate CI run with test results
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
+3
View File
@@ -5,6 +5,9 @@ on:
tags:
- '*'
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
sentry_release:
runs-on: ubuntu-latest
-2
View File
@@ -6,8 +6,6 @@ T:System.IComparable;Don't use non-generic IComparable. Use generic version inst
T:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable<T> instead.
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead.
@@ -12,7 +12,6 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
@@ -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" />
@@ -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" />
@@ -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" />
+1 -1
View File
@@ -52,7 +52,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.831.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.908.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.922.0" />
</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. -->
+33 -79
View File
@@ -5,26 +5,24 @@ using System;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
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 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");
@@ -35,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)
{
@@ -63,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;
}
@@ -75,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);
@@ -107,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!");
}
}
@@ -131,78 +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 SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.Upload,
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;
-5
View File
@@ -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">
@@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Catch.Tests
});
}
private class TestSkin : DefaultSkin
private class TestSkin : TrianglesSkin
{
public bool FlipCatcherPlate { get; set; }
@@ -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());
}
}
}
@@ -21,7 +21,6 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
using osuTK;
@@ -106,20 +105,37 @@ namespace osu.Game.Rulesets.Catch.Tests
public void TestCatcherCatchWidth()
{
float halfWidth = Catcher.CalculateCatchWidth(new BeatmapDifficulty { CircleSize = 0 }) / 2;
AddStep("move catcher to center", () => catcher.X = CatchPlayfield.CENTER_X);
float leftPlateBounds = CatchPlayfield.CENTER_X - halfWidth;
float rightPlateBounds = CatchPlayfield.CENTER_X + halfWidth;
AddStep("catch fruit", () =>
{
attemptCatch(new Fruit { X = -halfWidth + 1 });
attemptCatch(new Fruit { X = halfWidth - 1 });
attemptCatch(new Fruit { X = leftPlateBounds + 1 });
attemptCatch(new Fruit { X = rightPlateBounds - 1 });
});
checkPlate(2);
AddStep("miss fruit", () =>
{
attemptCatch(new Fruit { X = -halfWidth - 1 });
attemptCatch(new Fruit { X = halfWidth + 1 });
attemptCatch(new Fruit { X = leftPlateBounds - 1 });
attemptCatch(new Fruit { X = rightPlateBounds + 1 });
});
checkPlate(2);
}
[Test]
public void TestFruitClampedToCatchableRegion()
{
AddStep("catch fruit left", () => attemptCatch(new Fruit { X = -CatchPlayfield.WIDTH }));
checkPlate(1);
AddStep("move catcher to right", () => catcher.X = CatchPlayfield.WIDTH);
AddStep("catch fruit right", () => attemptCatch(new Fruit { X = CatchPlayfield.WIDTH * 2 }));
checkPlate(2);
}
[Test]
public void TestFruitChangesCatcherState()
{
@@ -233,11 +249,9 @@ namespace osu.Game.Rulesets.Catch.Tests
[Test]
public void TestHitLightingColour()
{
var fruitColour = SkinConfiguration.DefaultComboColours[1];
AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddAssert("correct hit lighting colour", () =>
catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == fruitColour);
AddAssert("correct hit lighting colour", () => catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == this.ChildrenOfType<DrawableCatchHitObject>().First().AccentColour.Value);
}
[Test]
@@ -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)
+11 -4
View File
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using osu.Framework.Extensions.EnumExtensions;
@@ -33,7 +31,7 @@ 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();
@@ -184,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();
@@ -17,21 +17,15 @@ namespace osu.Game.Rulesets.Catch.Mods
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
{
MinValue = 0.5f,
MaxValue = 1.5f,
Default = 1f,
Value = 1f,
Precision = 0.1f
};
[SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
public override BindableBool ComboBasedSize { get; } = new BindableBool
{
Default = true,
Value = true
};
public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
public override float DefaultFlashlightSize => 350;
@@ -22,10 +22,8 @@ namespace osu.Game.Rulesets.Catch.Mods
"The combo count at which the catcher becomes completely hidden",
SettingControlType = typeof(SettingsSlider<int, HiddenComboSlider>)
)]
public override BindableInt HiddenComboCount { get; } = new BindableInt
public override BindableInt HiddenComboCount { get; } = new BindableInt(10)
{
Default = 10,
Value = 10,
MinValue = 0,
MaxValue = 50,
};
@@ -3,6 +3,7 @@
#nullable disable
using System;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
@@ -69,7 +70,7 @@ namespace osu.Game.Rulesets.Catch.Objects
/// This value is the original <see cref="X"/> value plus the offset applied by the beatmap processing.
/// Use <see cref="OriginalX"/> if a value not affected by the offset is desired.
/// </remarks>
public float EffectiveX => OriginalX + XOffset;
public float EffectiveX => Math.Clamp(OriginalX + XOffset, 0, CatchPlayfield.WIDTH);
public double TimePreempt { get; set; } = 1000;
@@ -11,6 +11,7 @@ using Newtonsoft.Json;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -84,8 +85,8 @@ namespace osu.Game.Rulesets.Catch.Objects
AddNested(new TinyDroplet
{
StartTime = t + lastEvent.Value.Time,
X = OriginalX + Path.PositionAt(
lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X,
X = ClampToPlayfield(EffectiveX + Path.PositionAt(
lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X),
});
}
}
@@ -102,7 +103,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = dropletSamples,
StartTime = e.Time,
X = OriginalX + Path.PositionAt(e.PathProgress).X,
X = ClampToPlayfield(EffectiveX + Path.PositionAt(e.PathProgress).X),
});
break;
@@ -113,14 +114,16 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = this.GetNodeSamples(nodeIndex++),
StartTime = e.Time,
X = OriginalX + Path.PositionAt(e.PathProgress).X,
X = ClampToPlayfield(EffectiveX + Path.PositionAt(e.PathProgress).X),
});
break;
}
}
}
public float EndX => OriginalX + this.CurvePositionAt(1).X;
public float EndX => ClampToPlayfield(EffectiveX + this.CurvePositionAt(1).X);
public float ClampToPlayfield(float value) => Math.Clamp(value, 0, CatchPlayfield.WIDTH);
[JsonIgnore]
public double Duration
@@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
[BackgroundDependencyLoader]
private void load(SkinManager skins)
{
var defaultLegacySkin = skins.DefaultLegacySkin;
var defaultLegacySkin = skins.DefaultClassicSkin;
// sprite names intentionally swapped to match stable member naming / ease of cross-referencing
explosion1.Texture = defaultLegacySkin.GetTexture("scoreboard-explosion-2");
@@ -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,
}
}
}
@@ -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);
@@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test]
public void TestDefaultSkin()
{
AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLiveUnmanaged());
AddStep("set default skin", () => skins.CurrentSkinInfo.Value = TrianglesSkin.CreateInfo().ToLiveUnmanaged());
}
[Test]
@@ -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>
@@ -11,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)
{
}
+12 -5
View File
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
@@ -48,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();
@@ -64,7 +62,16 @@ namespace osu.Game.Rulesets.Mania
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)
{
@@ -285,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);
@@ -18,21 +18,15 @@ namespace osu.Game.Rulesets.Mania.Mods
public override Type[] IncompatibleMods => new[] { typeof(ModHidden) };
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
{
MinValue = 0.5f,
MaxValue = 3f,
Default = 1f,
Value = 1f,
Precision = 0.1f
};
[SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
public override BindableBool ComboBasedSize { get; } = new BindableBool
{
Default = false,
Value = false
};
public override BindableBool ComboBasedSize { get; } = new BindableBool();
public override float DefaultFlashlightSize => 50;
@@ -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()
});
}
}
@@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
@@ -88,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
if (!objects.Any())
return false;
return objects.All(o => Precision.AlmostEquals(o.ChildrenOfType<ShakeContainer>().First().Children.OfType<Container>().Single().Scale.X, target));
return objects.All(o => Precision.AlmostEquals(o.ChildrenOfType<Container>().First().Scale.X, target));
}
private bool checkSomeHit()
@@ -0,0 +1,102 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModRandom : OsuModTestScene
{
[TestCase(1)]
[TestCase(7)]
[TestCase(10)]
public void TestDefaultBeatmap(float angleSharpness) => CreateModTest(new ModTestData
{
Mod = new OsuModRandom
{
AngleSharpness = { Value = angleSharpness }
},
Autoplay = true,
PassCondition = () => true
});
[TestCase(1)]
[TestCase(7)]
[TestCase(10)]
public void TestJumpBeatmap(float angleSharpness) => CreateModTest(new ModTestData
{
Mod = new OsuModRandom
{
AngleSharpness = { Value = angleSharpness }
},
Beatmap = jumpBeatmap,
Autoplay = true,
PassCondition = () => true
});
[TestCase(1)]
[TestCase(7)]
[TestCase(10)]
public void TestStreamBeatmap(float angleSharpness) => CreateModTest(new ModTestData
{
Mod = new OsuModRandom
{
AngleSharpness = { Value = angleSharpness }
},
Beatmap = streamBeatmap,
Autoplay = true,
PassCondition = () => true
});
private OsuBeatmap jumpBeatmap =>
createHitCircleBeatmap(new[] { 100, 200, 300, 400 }, 8, 300, 2 * 300);
private OsuBeatmap streamBeatmap =>
createHitCircleBeatmap(new[] { 10, 20, 30, 40, 50, 60, 70, 80 }, 16, 150, 4 * 150);
private OsuBeatmap createHitCircleBeatmap(IEnumerable<int> spacings, int objectsPerSpacing, int interval, int beatLength)
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint
{
Time = 0,
BeatLength = beatLength
});
var beatmap = new OsuBeatmap
{
BeatmapInfo = new BeatmapInfo
{
StackLeniency = 0,
Difficulty = new BeatmapDifficulty
{
ApproachRate = 8.5f
}
},
ControlPointInfo = controlPointInfo
};
foreach (int spacing in spacings)
{
for (int i = 0; i < objectsPerSpacing; i++)
{
beatmap.HitObjects.Add(new HitCircle
{
StartTime = interval * beatmap.HitObjects.Count,
Position = beatmap.HitObjects.Count % 2 == 0 ? Vector2.Zero : new Vector2(spacing, 0),
NewCombo = i == 0
});
}
}
return beatmap;
}
}
}
@@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Tests
});
AddStep("setup default legacy skin", () =>
{
skinManager.CurrentSkinInfo.Value = skinManager.DefaultLegacySkin.SkinInfo;
skinManager.CurrentSkinInfo.Value = skinManager.DefaultClassicSkin.SkinInfo;
});
});
}
@@ -58,10 +58,11 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
{
var drawable = createSingle(circleSize, auto, timeOffset, positionOffset);
var playfield = new TestOsuPlayfield();
playfield.Add(drawable);
for (double t = timeOffset; t < timeOffset + 60000; t += 2000)
playfield.Add(createSingle(circleSize, auto, t, positionOffset));
return playfield;
}
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Tests
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
Child = new SkinProvidingContainer(new DefaultSkin(null))
Child = new SkinProvidingContainer(new TrianglesSkin(null))
{
RelativeSizeAxes = Axes.Both,
Child = drawableHitCircle = new DrawableHitCircle(hitCircle)
@@ -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)
{
@@ -7,7 +7,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
@@ -43,7 +42,6 @@ namespace osu.Game.Rulesets.Osu.Tests
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
private DrawableSpinner drawableSpinner = null!;
private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType<SpriteIcon>().Single();
[SetUpSteps]
public override void SetUpSteps()
@@ -77,7 +75,6 @@ namespace osu.Game.Rulesets.Osu.Tests
{
double finalCumulativeTrackerRotation = 0;
double finalTrackerRotation = 0, trackerRotationTolerance = 0;
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
addSeekStep(spinner_start_time + 5000);
AddStep("retrieve disc rotation", () =>
@@ -85,11 +82,6 @@ namespace osu.Game.Rulesets.Osu.Tests
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
trackerRotationTolerance = Math.Abs(finalTrackerRotation * 0.05f);
});
AddStep("retrieve spinner symbol rotation", () =>
{
finalSpinnerSymbolRotation = spinnerSymbol.Rotation;
spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f);
});
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
addSeekStep(spinner_start_time + 2500);
@@ -98,8 +90,6 @@ namespace osu.Game.Rulesets.Osu.Tests
// due to the exponential damping applied we're allowing a larger margin of error of about 10%
// (5% relative to the final rotation value, but we're half-way through the spin).
() => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation / 2).Within(trackerRotationTolerance));
AddAssert("symbol rotation rewound",
() => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation / 2).Within(spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation rewound",
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
() => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100));
@@ -107,8 +97,6 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(spinner_start_time + 5000);
AddAssert("is disc rotation almost same",
() => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation).Within(trackerRotationTolerance));
AddAssert("is symbol rotation almost same",
() => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation).Within(spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation almost same",
() => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100));
}
@@ -122,7 +110,6 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(5000);
AddAssert("disc spin direction correct", () => clockwise ? drawableSpinner.RotationTracker.Rotation > 0 : drawableSpinner.RotationTracker.Rotation < 0);
AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0);
}
private Replay flip(Replay scoreReplay) => new Replay
@@ -0,0 +1,149 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Replays;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneTrianglesSpinnerRotation : TestSceneOsuPlayer
{
private const double spinner_start_time = 100;
private const double spinner_duration = 6000;
[Resolved]
private SkinManager skinManager { get; set; } = null!;
[Resolved]
private AudioManager audioManager { get; set; } = null!;
protected override bool Autoplay => true;
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
private DrawableSpinner drawableSpinner = null!;
private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType<SpriteIcon>().Single();
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("set triangles skin", () => skinManager.CurrentSkinInfo.Value = TrianglesSkin.CreateInfo().ToLiveUnmanaged());
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First());
}
[Test]
public void TestSymbolMiddleRewindingRotation()
{
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
addSeekStep(spinner_start_time + 5000);
AddStep("retrieve spinner symbol rotation", () =>
{
finalSpinnerSymbolRotation = spinnerSymbol.Rotation;
spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f);
});
addSeekStep(spinner_start_time + 2500);
AddAssert("symbol rotation rewound",
() => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation / 2).Within(spinnerSymbolRotationTolerance));
addSeekStep(spinner_start_time + 5000);
AddAssert("is symbol rotation almost same",
() => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation).Within(spinnerSymbolRotationTolerance));
}
[Test]
public void TestSymbolRotationDirection([Values(true, false)] bool clockwise)
{
if (clockwise)
transformReplay(flip);
addSeekStep(5000);
AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0);
}
private Replay flip(Replay scoreReplay) => new Replay
{
Frames = scoreReplay
.Frames
.Cast<OsuReplayFrame>()
.Select(replayFrame =>
{
var flippedPosition = new Vector2(OsuPlayfield.BASE_SIZE.X - replayFrame.Position.X, replayFrame.Position.Y);
return new OsuReplayFrame(replayFrame.Time, flippedPosition, replayFrame.Actions.ToArray());
})
.Cast<ReplayFrame>()
.ToList()
};
private void addSeekStep(double time)
{
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(100));
}
private void transformReplay(Func<Replay, Replay> replayTransformation) => AddStep("set replay", () =>
{
var drawableRuleset = this.ChildrenOfType<DrawableOsuRuleset>().Single();
var score = drawableRuleset.ReplayScore;
var transformedScore = new Score
{
ScoreInfo = score.ScoreInfo,
Replay = replayTransformation.Invoke(score.Replay)
};
drawableRuleset.SetReplayScore(transformedScore);
});
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
{
HitObjects = new List<HitObject>
{
new Spinner
{
Position = new Vector2(256, 192),
StartTime = spinner_start_time,
Duration = spinner_duration
},
}
};
private class ScoreExposedPlayer : TestPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
public ScoreExposedPlayer()
: base(false, false)
{
}
}
}
}
@@ -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>
@@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
if (!(currentObj.BaseObject is Spinner))
{
double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.EndPosition).Length;
double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.StackedEndPosition).Length;
cumulativeStrainTime += lastObj.StrainTime;
@@ -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,7 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public class OsuDifficultyCalculator : DifficultyCalculator
{
private const double difficulty_multiplier = 0.0675;
private double hitWindowGreat;
public override int Version => 20220902;
@@ -45,6 +44,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
if (mods.Any(m => m is OsuModTouchDevice))
{
aimRating = Math.Pow(aimRating, 0.8);
flashlightRating = Math.Pow(flashlightRating, 0.8);
}
if (mods.Any(h => h is OsuModRelax))
{
aimRating *= 0.9;
@@ -76,6 +81,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,22 +122,18 @@ 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)
};
}
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
{
new OsuModTouchDevice(),
new OsuModDoubleTime(),
new OsuModHalfTime(),
new OsuModEasy(),
@@ -88,12 +88,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
double rawAim = attributes.AimDifficulty;
if (score.Mods.Any(m => m is OsuModTouchDevice))
rawAim = Math.Pow(rawAim, 0.8);
double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
@@ -233,12 +228,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (!score.Mods.Any(h => h is OsuModFlashlight))
return 0.0;
double rawFlashlight = attributes.FlashlightDifficulty;
if (score.Mods.Any(m => m is OsuModTouchDevice))
rawFlashlight = Math.Pow(rawFlashlight, 0.8);
double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0;
double flashlightValue = Math.Pow(attributes.FlashlightDifficulty, 2.0) * 25.0;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)
@@ -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,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;
@@ -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;
@@ -303,11 +303,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
else
{
var result = snapProvider?.FindSnappedPositionAndTime(Parent.ToScreenSpace(e.MousePosition));
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? Parent.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - slider.Position;
for (int i = 0; i < controlPoints.Count; ++i)
{
var controlPoint = controlPoints[i];
if (selectedControlPoints.Contains(controlPoint))
controlPoint.Position = dragStartPositions[i] + (e.MousePosition - e.MouseDownPosition);
controlPoint.Position = dragStartPositions[i] + movementDelta;
}
}
@@ -198,7 +198,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
// Update the cursor position.
cursor.Position = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
var result = snapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
}
else if (cursor != null)
{
@@ -163,7 +163,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected override void OnDrag(DragEvent e)
{
if (placementControlPoint != null)
placementControlPoint.Position = e.MousePosition - HitObject.Position;
{
var result = snapProvider?.FindSnappedPositionAndTime(ToScreenSpace(e.MousePosition));
placementControlPoint.Position = ToLocalSpace(result?.ScreenSpacePosition ?? ToScreenSpace(e.MousePosition)) - HitObject.Position;
}
}
protected override void OnMouseUp(MouseUpEvent e)
@@ -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);
}
}
@@ -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)
{
+1 -3
View File
@@ -19,12 +19,10 @@ namespace osu.Game.Rulesets.Osu.Mods
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
public override BindableNumber<float> StartScale { get; } = new BindableFloat(2)
{
MinValue = 1f,
MaxValue = 25f,
Default = 2f,
Value = 2f,
Precision = 0.1f,
};
}
@@ -33,21 +33,15 @@ namespace osu.Game.Rulesets.Osu.Mods
};
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
{
MinValue = 0.5f,
MaxValue = 2f,
Default = 1f,
Value = 1f,
Precision = 0.1f
};
[SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
public override BindableBool ComboBasedSize { get; } = new BindableBool
{
Default = true,
Value = true
};
public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
public override float DefaultFlashlightSize => 180;
+1 -3
View File
@@ -19,12 +19,10 @@ namespace osu.Game.Rulesets.Osu.Mods
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
public override BindableNumber<float> StartScale { get; } = new BindableFloat(0.5f)
{
MinValue = 0f,
MaxValue = 0.99f,
Default = 0.5f,
Value = 0.5f,
Precision = 0.01f,
};
}
+1 -3
View File
@@ -27,10 +27,8 @@ namespace osu.Game.Rulesets.Osu.Mods
"The combo count at which the cursor becomes completely hidden",
SettingControlType = typeof(SettingsSlider<int, HiddenComboSlider>)
)]
public override BindableInt HiddenComboCount { get; } = new BindableInt
public override BindableInt HiddenComboCount { get; } = new BindableInt(10)
{
Default = 10,
Value = 10,
MinValue = 0,
MaxValue = 50,
};
+110 -14
View File
@@ -2,13 +2,18 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
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;
@@ -23,42 +28,133 @@ namespace osu.Game.Rulesets.Osu.Mods
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray();
[SettingSource("Angle sharpness", "How sharp angles should be", SettingControlType = typeof(SettingsSlider<float>))]
public BindableFloat AngleSharpness { get; } = new BindableFloat(7)
{
MinValue = 1,
MaxValue = 10,
Precision = 0.1f
};
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 = getRandomOffset(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 = getRandomOffset(0.002f);
if (shouldApplyFlowChange(positionInfos, i))
{
flowChangeOffset = getRandomOffset(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);
}
private float getRandomOffset(float stdDev)
{
// Range: [0.5, 2]
// Higher angle sharpness -> lower multiplier
float customMultiplier = (1.5f * AngleSharpness.MaxValue - AngleSharpness.Value) / (1.5f * AngleSharpness.MaxValue - AngleSharpness.Default);
return OsuHitObjectGenerationUtils.RandomGaussian(random, 0, stdDev * customMultiplier);
}
/// <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 float getRelativeTargetAngle(float targetDistance, float offset, bool flowDirection)
{
// Range: [0.1, 1]
float angleSharpness = AngleSharpness.Value / AngleSharpness.MaxValue;
// Range: [0, 0.9]
float angleWideness = 1 - angleSharpness;
// Range: [-60, 30]
float customOffsetX = angleSharpness * 100 - 70;
// Range: [-0.075, 0.15]
float customOffsetY = angleWideness * 0.25f - 0.075f;
targetDistance += customOffsetX;
float angle = (float)(2.16 / (1 + 200 * Math.Exp(0.036 * (targetDistance - 310 + customOffsetX))) + 0.5);
angle += offset + customOffsetY;
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;
}
}
}
+1 -5
View File
@@ -53,11 +53,7 @@ namespace osu.Game.Rulesets.Osu.Mods
}).ToArray();
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
public Bindable<int?> Seed { get; } = new Bindable<int?>
{
Default = null,
Value = null
};
public Bindable<int?> Seed { get; } = new Bindable<int?>();
[SettingSource("Metronome ticks", "Whether a metronome beat should play in the background")]
public Bindable<bool> Metronome { get; } = new BindableBool(true);
@@ -1,9 +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.
#nullable disable
using System;
using System.Diagnostics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects;
@@ -21,32 +20,36 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
public const int SPACING = 32;
public const double PREEMPT = 800;
public DrawablePool<FollowPoint> Pool;
public DrawablePool<FollowPoint>? Pool { private get; set; }
protected override void OnApply(FollowPointLifetimeEntry entry)
{
base.OnApply(entry);
entry.Invalidated += onEntryInvalidated;
refreshPoints();
entry.Invalidated += scheduleRefresh;
// Our clock may not be correct at this point if `LoadComplete` has not run yet.
// Without a schedule, animations referencing FollowPoint's clock (see `IAnimationTimeReference`) would be incorrect on first pool usage.
scheduleRefresh();
}
protected override void OnFree(FollowPointLifetimeEntry entry)
{
base.OnFree(entry);
entry.Invalidated -= onEntryInvalidated;
entry.Invalidated -= scheduleRefresh;
// Return points to the pool.
ClearInternal(false);
}
private void onEntryInvalidated() => Scheduler.AddOnce(refreshPoints);
private void refreshPoints()
private void scheduleRefresh() => Scheduler.AddOnce(() =>
{
Debug.Assert(Pool != null);
ClearInternal(false);
var entry = Entry;
if (entry?.End == null) return;
OsuHitObject start = entry.Start;
@@ -95,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
}
entry.LifetimeEnd = finalTransformEndTime;
}
});
/// <summary>
/// Computes the fade time of follow point positioned between two hitobjects.
@@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
@@ -47,12 +48,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
}
private ShakeContainer shakeContainer;
[BackgroundDependencyLoader]
private void load()
{
Origin = Anchor.Centre;
InternalChildren = new Drawable[]
AddRangeInternal(new Drawable[]
{
scaleContainer = new Container
{
@@ -72,22 +75,30 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return true;
},
},
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece())
shakeContainer = new ShakeContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
ApproachCircle = new ProxyableSkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.ApproachCircle), _ => new DefaultApproachCircle())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
ShakeDuration = 30,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Scale = new Vector2(4),
Children = new Drawable[]
{
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
ApproachCircle = new ProxyableSkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.ApproachCircle), _ => new DefaultApproachCircle())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Scale = new Vector2(4),
}
}
}
}
},
};
});
Size = HitArea.DrawSize;
@@ -123,6 +134,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
public override void Shake() => shakeContainer.Shake();
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
Debug.Assert(HitObject.HitWindows != null);
@@ -139,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
{
Shake(Math.Abs(timeOffset) - HitObject.HitWindows.WindowFor(HitResult.Miss));
Shake();
return;
}
@@ -6,17 +6,16 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableOsuHitObject : DrawableHitObject<OsuHitObject>
public abstract class DrawableOsuHitObject : DrawableHitObject<OsuHitObject>
{
public readonly IBindable<Vector2> PositionBindable = new Bindable<Vector2>();
public readonly IBindable<int> StackHeightBindable = new Bindable<int>();
@@ -34,8 +33,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// </summary>
public Func<DrawableHitObject, double, bool> CheckHittable;
private ShakeContainer shakeContainer;
protected DrawableOsuHitObject(OsuHitObject hitObject)
: base(hitObject)
{
@@ -45,12 +42,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void load()
{
Alpha = 0;
base.AddInternal(shakeContainer = new ShakeContainer
{
ShakeDuration = 30,
RelativeSizeAxes = Axes.Both
});
}
protected override void OnApply()
@@ -73,18 +64,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ScaleBindable.UnbindFrom(HitObject.ScaleBindable);
}
// Forward all internal management to shakeContainer.
// This is a bit ugly but we don't have the concept of InternalContent so it'll have to do for now. (https://github.com/ppy/osu-framework/issues/1690)
protected override void AddInternal(Drawable drawable) => shakeContainer.Add(drawable);
protected override void ClearInternal(bool disposeChildren = true) => shakeContainer.Clear(disposeChildren);
protected override bool RemoveInternal(Drawable drawable, bool disposeImmediately) => shakeContainer.Remove(drawable, disposeImmediately);
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
private OsuInputManager osuActionInputManager;
internal OsuInputManager OsuActionInputManager => osuActionInputManager ??= GetContainingInputManager() as OsuInputManager;
public virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength);
/// <summary>
/// Shake the hit object in case it was clicked far too early or late (aka "note lock").
/// </summary>
public virtual void Shake() { }
/// <summary>
/// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
@@ -11,6 +11,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning;
@@ -34,6 +35,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public SkinnableDrawable Body { get; private set; }
private ShakeContainer shakeContainer;
/// <summary>
/// A target container which can be used to add top level elements to the slider's display.
/// Intended to be used for proxy purposes only.
@@ -74,17 +77,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
AddRangeInternal(new Drawable[]
{
Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
shakeContainer = new ShakeContainer
{
ShakeDuration = 30,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
}
},
// slider head is not included in shake as it handles hit detection, and handles its own shaking.
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, },
Ball,
slidingSample = new PausableSkinnableSound { Looping = true }
};
});
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
@@ -109,6 +121,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
PathVersion.BindTo(HitObject.Path.Version);
}
public override void Shake() => shakeContainer.Shake();
protected override void OnFree()
{
base.OnFree();
@@ -63,7 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
pathVersion.BindTo(DrawableSlider.PathVersion);
OnShake = DrawableSlider.Shake;
CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? true;
}
@@ -96,9 +95,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return result.IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss;
}
public Action<double> OnShake;
public override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength);
public override void Shake()
{
base.Shake();
DrawableSlider.Shake();
}
private void updatePosition()
{
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
InternalChild = scaleContainer = new Container
AddInternal(scaleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
},
Arrow = new ReverseArrowPiece(),
}
};
});
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
InternalChildren = new Drawable[]
AddRangeInternal(new Drawable[]
{
scaleContainer = new Container
{
@@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty())
}
},
};
});
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
@@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
Origin = Anchor.Centre;
InternalChild = scaleContainer = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderScorePoint), _ => new CircularContainer
AddInternal(scaleContainer = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderScorePoint), _ => new CircularContainer
{
Masking = true,
Origin = Anchor.Centre,
@@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
});
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
+1 -1
View File
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Objects
// This is so on repeats ticks don't appear too late to be visually processed by the player.
offset = 200;
else
offset = TimeFadeIn * 0.66f;
offset = TimePreempt * 0.66f;
TimePreempt = (StartTime - SpanStartTime) / 2 + offset;
}
+16 -5
View File
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
@@ -29,6 +27,7 @@ using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.Skinning.Argon;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Rulesets.Osu.UI;
@@ -44,7 +43,7 @@ namespace osu.Game.Rulesets.Osu
{
public class OsuRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableOsuRuleset(this, beatmap, mods);
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableOsuRuleset(this, beatmap, mods);
public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor();
@@ -233,13 +232,25 @@ namespace osu.Game.Rulesets.Osu
public override RulesetSettingsSubsection CreateSettings() => new OsuSettingsSubsection(this);
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new OsuLegacySkinTransformer(skin);
public override ISkin? CreateSkinTransformer(ISkin skin, IBeatmap beatmap)
{
switch (skin)
{
case LegacySkin:
return new OsuLegacySkinTransformer(skin);
case ArgonSkin:
return new OsuArgonSkinTransformer(skin);
}
return null;
}
public int LegacyID => 0;
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
protected override IEnumerable<HitResult> GetValidHitResults()
{
@@ -0,0 +1,80 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonCursor : OsuCursorSprite
{
public ArgonCursor()
{
RelativeSizeAxes = Axes.Both;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
InternalChildren = new[]
{
ExpandTarget = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 6,
BorderColour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.4f,
Colour = Colour4.FromHex("FC618F").Darken(0.6f),
},
new CircularContainer
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 2,
BorderColour = Color4.White.Opacity(0.8f),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true,
},
},
},
},
},
new Circle
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Scale = new Vector2(0.2f),
Colour = new Color4(255, 255, 255, 255),
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 20,
Colour = new Color4(171, 255, 255, 100),
},
},
};
}
}
}
@@ -0,0 +1,29 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonCursorTrail : CursorTrail
{
protected override float IntervalMultiplier => 0.4f;
protected override float FadeExponent => 4;
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
Texture = textures.Get(@"Cursor/cursortrail");
Scale = new Vector2(0.8f / Texture.ScaleAdjust);
Blending = BlendingParameters.Additive;
Alpha = 0.8f;
}
}
}
@@ -0,0 +1,71 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonFollowCircle : FollowCircle
{
public ArgonFollowCircle()
{
InternalChild = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 4,
BorderColour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
Blending = BlendingParameters.Additive,
Child = new Box
{
Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
RelativeSizeAxes = Axes.Both,
Alpha = 0.3f,
}
};
}
protected override void OnSliderPress()
{
const float duration = 300f;
if (Precision.AlmostEquals(0, Alpha))
this.ScaleTo(1);
this.ScaleTo(DrawableSliderBall.FOLLOW_AREA, duration, Easing.OutQuint)
.FadeIn(duration, Easing.OutQuint);
}
protected override void OnSliderRelease()
{
const float duration = 150;
this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.2f, duration, Easing.OutQuint)
.FadeTo(0, duration, Easing.OutQuint);
}
protected override void OnSliderEnd()
{
const float duration = 300;
this.ScaleTo(1, duration, Easing.OutQuint)
.FadeOut(duration / 2, Easing.OutQuint);
}
protected override void OnSliderTick()
{
this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint)
.Then()
.ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint);
}
protected override void OnSliderBreak()
{
}
}
}
@@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonFollowPoint : CompositeDrawable
{
public ArgonFollowPoint()
{
Blending = BlendingParameters.Additive;
Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41"));
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new SpriteIcon
{
Icon = FontAwesome.Solid.ChevronRight,
Size = new Vector2(8),
Colour = OsuColour.Gray(0.2f),
},
new SpriteIcon
{
Icon = FontAwesome.Solid.ChevronRight,
Size = new Vector2(8),
X = 4,
},
};
}
}
}
@@ -0,0 +1,171 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
{
protected readonly HitResult Result;
protected SpriteText JudgementText { get; private set; } = null!;
private RingExplosion? ringExplosion;
[Resolved]
private OsuColour colours { get; set; } = null!;
public ArgonJudgementPiece(HitResult result)
{
Result = result;
Origin = Anchor.Centre;
}
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
JudgementText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = Result.GetDescription().ToUpperInvariant(),
Colour = colours.ForHitResult(Result),
Blending = BlendingParameters.Additive,
Spacing = new Vector2(5, 0),
Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold),
},
};
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
{
Colour = colours.ForHitResult(Result),
});
}
}
/// <summary>
/// Plays the default animation for this judgement piece.
/// </summary>
/// <remarks>
/// The base implementation only handles fade (for all result types) and misses.
/// Individual rulesets are recommended to implement their appropriate hit animations.
/// </remarks>
public virtual void PlayAnimation()
{
switch (Result)
{
default:
JudgementText
.ScaleTo(Vector2.One)
.ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint);
break;
case HitResult.Miss:
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
this.MoveTo(Vector2.Zero);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(0);
this.RotateTo(40, 800, Easing.InQuint);
break;
}
this.FadeOutFromOne(800);
ringExplosion?.PlayAnimation();
}
public Drawable? GetAboveHitObjectsProxiedContent() => null;
private class RingExplosion : CompositeDrawable
{
private readonly float travel = 52;
public RingExplosion(HitResult result)
{
const float thickness = 4;
const float small_size = 9;
const float large_size = 14;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Blending = BlendingParameters.Additive;
int countSmall = 0;
int countLarge = 0;
switch (result)
{
case HitResult.Meh:
countSmall = 3;
travel *= 0.3f;
break;
case HitResult.Ok:
case HitResult.Good:
countSmall = 4;
travel *= 0.6f;
break;
case HitResult.Great:
case HitResult.Perfect:
countSmall = 4;
countLarge = 4;
break;
}
for (int i = 0; i < countSmall; i++)
AddInternal(new RingPiece(thickness) { Size = new Vector2(small_size) });
for (int i = 0; i < countLarge; i++)
AddInternal(new RingPiece(thickness) { Size = new Vector2(large_size) });
}
public void PlayAnimation()
{
foreach (var c in InternalChildren)
{
const float start_position_ratio = 0.3f;
float direction = RNG.NextSingle(0, 360);
float distance = RNG.NextSingle(travel / 2, travel);
c.MoveTo(new Vector2(
MathF.Cos(direction) * distance * start_position_ratio,
MathF.Sin(direction) * distance * start_position_ratio
));
c.MoveTo(new Vector2(
MathF.Cos(direction) * distance,
MathF.Sin(direction) * distance
), 600, Easing.OutQuint);
}
this.FadeOutFromOne(1000, Easing.OutQuint);
}
}
}
}
@@ -0,0 +1,226 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonMainCirclePiece : CompositeDrawable
{
public const float BORDER_THICKNESS = (OsuHitObject.OBJECT_RADIUS * 2) * (2f / 58);
public const float GRADIENT_THICKNESS = BORDER_THICKNESS * 2.5f;
public const float OUTER_GRADIENT_SIZE = (OsuHitObject.OBJECT_RADIUS * 2) - BORDER_THICKNESS * 4;
public const float INNER_GRADIENT_SIZE = OUTER_GRADIENT_SIZE - GRADIENT_THICKNESS * 2;
public const float INNER_FILL_SIZE = INNER_GRADIENT_SIZE - GRADIENT_THICKNESS * 2;
private readonly Circle outerFill;
private readonly Circle outerGradient;
private readonly Circle innerGradient;
private readonly Circle innerFill;
private readonly RingPiece border;
private readonly OsuSpriteText number;
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
private readonly FlashPiece flash;
[Resolved]
private DrawableHitObject drawableObject { get; set; } = null!;
public ArgonMainCirclePiece(bool withOuterFill)
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
InternalChildren = new Drawable[]
{
outerFill = new Circle // renders white outer border and dark fill
{
Size = Size,
Alpha = withOuterFill ? 1 : 0,
},
outerGradient = new Circle // renders the outer bright gradient
{
Size = new Vector2(OUTER_GRADIENT_SIZE),
Alpha = 1,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
innerGradient = new Circle // renders the inner bright gradient
{
Size = new Vector2(INNER_GRADIENT_SIZE),
Alpha = 1,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
innerFill = new Circle // renders the inner dark fill
{
Size = new Vector2(INNER_FILL_SIZE),
Alpha = 1,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
number = new OsuSpriteText
{
Font = OsuFont.Default.With(size: 52, weight: FontWeight.Bold),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = -2,
Text = @"1",
},
flash = new FlashPiece(),
border = new RingPiece(BORDER_THICKNESS),
};
}
[BackgroundDependencyLoader]
private void load()
{
var drawableOsuObject = (DrawableOsuHitObject)drawableObject;
accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
}
protected override void LoadComplete()
{
base.LoadComplete();
accentColour.BindValueChanged(colour =>
{
outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4);
outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f));
innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
flash.Colour = colour.NewValue;
}, true);
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableObject, drawableObject.State.Value);
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
{
switch (state)
{
case ArmedState.Hit:
// Fade out time is at a maximum of 800. Must match `DrawableHitCircle`'s arbitrary lifetime spec.
const double fade_out_time = 800;
const double flash_in_duration = 150;
const double resize_duration = 400;
const float shrink_size = 0.8f;
// Animating with the number present is distracting.
// The number disappearing is hidden by the bright flash.
number.FadeOut(flash_in_duration / 2);
// The fill layers add too much noise during the explosion animation.
// They will be hidden by the additive effects anyway.
outerFill.FadeOut(flash_in_duration, Easing.OutQuint);
innerFill.FadeOut(flash_in_duration, Easing.OutQuint);
// The inner-most gradient should actually be resizing, but is only visible for
// a few milliseconds before it's hidden by the flash, so it's pointless overhead to bother with it.
innerGradient.FadeOut(flash_in_duration, Easing.OutQuint);
// The border is always white, but after hit it gets coloured by the skin/beatmap's colouring.
// A gradient is applied to make the border less prominent over the course of the animation.
// Without this, the border dominates the visual presence of the explosion animation in a bad way.
border.TransformTo(nameof
(BorderColour), ColourInfo.GradientVertical(
accentColour.Value.Opacity(0.5f),
accentColour.Value.Opacity(0)), fade_out_time);
// The outer ring shrinks immediately, but accounts for its thickness so it doesn't overlap the inner
// gradient layers.
border.ResizeTo(Size * shrink_size + new Vector2(border.BorderThickness), resize_duration, Easing.OutElasticHalf);
// The outer gradient is resize with a slight delay from the border.
// This is to give it a bomb-like effect, with the border "triggering" its animation when getting close.
using (BeginDelayedSequence(flash_in_duration / 12))
{
outerGradient.ResizeTo(outerGradient.Size * shrink_size, resize_duration, Easing.OutElasticHalf);
outerGradient
.FadeColour(Color4.White, 80)
.Then()
.FadeOut(flash_in_duration);
}
// The flash layer starts white to give the wanted brightness, but is almost immediately
// recoloured to the accent colour. This would more correctly be done with two layers (one for the initial flash)
// but works well enough with the colour fade.
flash.FadeTo(1, flash_in_duration, Easing.OutQuint);
flash.FlashColour(accentColour.Value, fade_out_time, Easing.OutQuint);
this.FadeOut(fade_out_time, Easing.OutQuad);
break;
}
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableObject.IsNotNull())
drawableObject.ApplyCustomUpdateState -= updateStateTransforms;
}
private class FlashPiece : Circle
{
public FlashPiece()
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS);
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Alpha = 0;
Blending = BlendingParameters.Additive;
// The edge effect provides the fill due to not being rendered hollow.
Child.Alpha = 0;
Child.AlwaysPresent = true;
}
protected override void Update()
{
base.Update();
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = Colour,
Radius = OsuHitObject.OBJECT_RADIUS * 1.2f,
};
}
}
}
}
@@ -0,0 +1,54 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonReverseArrow : CompositeDrawable
{
private Bindable<Color4> accentColour = null!;
private SpriteIcon icon = null!;
[BackgroundDependencyLoader]
private void load(DrawableHitObject hitObject)
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
InternalChildren = new Drawable[]
{
new Circle
{
Size = new Vector2(40, 20),
Colour = Color4.White,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
icon = new SpriteIcon
{
Icon = FontAwesome.Solid.AngleDoubleRight,
Size = new Vector2(16),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
};
accentColour = hitObject.AccentColour.GetBoundCopy();
accentColour.BindValueChanged(accent => icon.Colour = accent.NewValue.Darken(4), true);
}
}
}
@@ -0,0 +1,109 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonSliderBall : CircularContainer
{
private readonly Box fill;
private readonly SpriteIcon icon;
private readonly Vector2 defaultIconScale = new Vector2(0.6f, 0.8f);
[Resolved(canBeNull: true)]
private DrawableHitObject? parentObject { get; set; }
public ArgonSliderBall()
{
Size = new Vector2(ArgonMainCirclePiece.OUTER_GRADIENT_SIZE);
Masking = true;
BorderThickness = ArgonMainCirclePiece.GRADIENT_THICKNESS;
BorderColour = Color4.White;
InternalChildren = new Drawable[]
{
fill = new Box
{
Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
icon = new SpriteIcon
{
Size = new Vector2(48),
Scale = defaultIconScale,
Icon = FontAwesome.Solid.AngleRight,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
if (parentObject != null)
{
parentObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(parentObject, parentObject.State.Value);
}
}
private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState _)
{
// Gets called by slider ticks, tails, etc., leading to duplicated
// animations which in this case have no visual impact (due to
// instant fade) but may negatively affect performance
if (drawableObject is not DrawableSlider)
return;
const float duration = 200;
const float icon_scale = 0.9f;
using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
{
this.FadeInFromZero(duration, Easing.OutQuint);
icon.ScaleTo(0).Then().ScaleTo(defaultIconScale, duration, Easing.OutElasticHalf);
}
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
{
this.FadeOut(duration, Easing.OutQuint);
icon.ScaleTo(defaultIconScale * icon_scale, duration, Easing.OutQuint);
}
}
protected override void Update()
{
base.Update();
//undo rotation on layers which should not be rotated.
float appliedRotation = Parent.Rotation;
fill.Rotation = -appliedRotation;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (parentObject != null)
parentObject.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}
@@ -0,0 +1,40 @@
// 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.Extensions.Color4Extensions;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonSliderBody : PlaySliderBody
{
protected override void LoadComplete()
{
const float path_radius = ArgonMainCirclePiece.OUTER_GRADIENT_SIZE / 2;
base.LoadComplete();
AccentColourBindable.BindValueChanged(accent => BorderColour = accent.NewValue, true);
ScaleBindable.BindValueChanged(scale => PathRadius = path_radius * scale.NewValue, true);
// This border size thing is kind of weird, hey.
const float intended_thickness = ArgonMainCirclePiece.GRADIENT_THICKNESS / path_radius;
BorderSize = intended_thickness / Default.DrawableSliderPath.BORDER_PORTION;
}
protected override Default.DrawableSliderPath CreateSliderPath() => new DrawableSliderPath();
private class DrawableSliderPath : Default.DrawableSliderPath
{
protected override Color4 ColourAt(float position)
{
if (CalculatedBorderPortion != 0f && position <= CalculatedBorderPortion)
return BorderColour;
return AccentColour.Darken(4);
}
}
}
}
@@ -0,0 +1,40 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonSliderScorePoint : CircularContainer
{
private Bindable<Color4> accentColour = null!;
private const float size = 12;
[BackgroundDependencyLoader]
private void load(DrawableHitObject hitObject)
{
Masking = true;
Origin = Anchor.Centre;
Size = new Vector2(size);
BorderThickness = 3;
BorderColour = Color4.White;
Child = new Box
{
RelativeSizeAxes = Axes.Both,
AlwaysPresent = true,
Alpha = 0,
};
accentColour = hitObject.AccentColour.GetBoundCopy();
accentColour.BindValueChanged(accent => BorderColour = accent.NewValue, true);
}
}
}
@@ -0,0 +1,146 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Globalization;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonSpinner : CompositeDrawable
{
private DrawableSpinner drawableSpinner = null!;
private OsuSpriteText bonusCounter = null!;
private Container spmContainer = null!;
private OsuSpriteText spmCounter = null!;
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableHitObject)
{
RelativeSizeAxes = Axes.Both;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
drawableSpinner = (DrawableSpinner)drawableHitObject;
InternalChildren = new Drawable[]
{
bonusCounter = new OsuSpriteText
{
Alpha = 0,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Default.With(size: 24),
Y = -120,
},
new ArgonSpinnerDisc
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
bonusCounter = new OsuSpriteText
{
Alpha = 0,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Default.With(size: 28, weight: FontWeight.Bold),
Y = -100,
},
spmContainer = new Container
{
Alpha = 0f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = 60,
Children = new[]
{
spmCounter = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = @"0",
Font = OsuFont.Default.With(size: 28, weight: FontWeight.SemiBold)
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = @"SPINS PER MINUTE",
Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
Y = 30
}
}
}
};
}
private IBindable<double> gainedBonus = null!;
private IBindable<double> spinsPerMinute = null!;
protected override void LoadComplete()
{
base.LoadComplete();
gainedBonus = drawableSpinner.GainedBonus.GetBoundCopy();
gainedBonus.BindValueChanged(bonus =>
{
bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo);
bonusCounter.FadeOutFromOne(1500);
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
});
spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy();
spinsPerMinute.BindValueChanged(spm =>
{
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
}, true);
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
}
protected override void Update()
{
base.Update();
if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
fadeCounterOnTimeStart();
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
if (!(drawableHitObject is DrawableSpinner))
return;
fadeCounterOnTimeStart();
}
private void fadeCounterOnTimeStart()
{
if (drawableSpinner.Result?.TimeStarted is double startTime)
{
using (BeginAbsoluteSequence(startTime))
spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableSpinner.IsNotNull())
drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}
@@ -0,0 +1,247 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonSpinnerDisc : CompositeDrawable
{
private const float initial_scale = 1f;
private const float idle_alpha = 0.2f;
private const float tracking_alpha = 0.4f;
private const float idle_centre_size = 80f;
private const float tracking_centre_size = 40f;
private DrawableSpinner drawableSpinner = null!;
private readonly BindableBool complete = new BindableBool();
private int wholeRotationCount;
private bool checkNewRotationCount
{
get
{
int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360);
if (wholeRotationCount == rotations) return false;
wholeRotationCount = rotations;
return true;
}
}
private Container disc = null!;
private Container centre = null!;
private CircularContainer fill = null!;
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableHitObject)
{
drawableSpinner = (DrawableSpinner)drawableHitObject;
// we are slightly bigger than our parent, to clip the top and bottom of the circle
// this should probably be revisited when scaled spinners are a thing.
Scale = new Vector2(initial_scale);
InternalChildren = new Drawable[]
{
disc = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
fill = new CircularContainer
{
Name = @"Fill",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Colour4.FromHex("FC618F").Opacity(1f),
Radius = 40,
},
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0f,
AlwaysPresent = true,
}
},
new CircularContainer
{
Name = @"Ring",
Masking = true,
BorderColour = Color4.White,
BorderThickness = 5,
RelativeSizeAxes = Axes.Both,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true,
}
},
new ArgonSpinnerTicks(),
}
},
centre = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(idle_centre_size),
Children = new[]
{
new RingPiece(10)
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.8f),
},
new RingPiece(3)
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(1f),
}
},
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
}
protected override void Update()
{
base.Update();
complete.Value = Time.Current >= drawableSpinner.Result.TimeCompleted;
if (complete.Value)
{
if (checkNewRotationCount)
{
fill.FinishTransforms(false, nameof(Alpha));
fill
.FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo)
.Then()
.FadeTo(tracking_alpha, 250, Easing.OutQuint);
}
}
else
{
fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime));
}
if (centre.Width == idle_centre_size && drawableSpinner.Result?.TimeStarted != null)
updateCentrePieceSize();
const float initial_fill_scale = 0.1f;
float targetScale = initial_fill_scale + (0.98f - initial_fill_scale) * drawableSpinner.Progress;
fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
disc.Rotation = drawableSpinner.RotationTracker.Rotation;
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
if (!(drawableHitObject is DrawableSpinner))
return;
Spinner spinner = drawableSpinner.HitObject;
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
this.ScaleTo(initial_scale);
this.RotateTo(0);
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
// constant ambient rotation to give the spinner "spinning" character.
this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
}
using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset))
{
switch (state)
{
case ArmedState.Hit:
this.ScaleTo(initial_scale * 1.2f, 320, Easing.Out);
this.RotateTo(Rotation + 180, 320);
break;
case ArmedState.Miss:
this.ScaleTo(initial_scale * 0.8f, 320, Easing.In);
break;
}
}
}
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
centre.ScaleTo(0);
disc.ScaleTo(0);
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
disc.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
centre.ScaleTo(0.8f, spinner.TimePreempt / 2, Easing.OutQuint);
disc.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
}
}
}
if (drawableSpinner.Result?.TimeStarted != null)
updateCentrePieceSize();
}
private void updateCentrePieceSize()
{
Debug.Assert(drawableSpinner.Result?.TimeStarted != null);
Spinner spinner = drawableSpinner.HitObject;
using (BeginAbsoluteSequence(drawableSpinner.Result.TimeStarted.Value))
centre.ResizeTo(new Vector2(tracking_centre_size), spinner.TimePreempt / 2, Easing.OutQuint);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableSpinner.IsNotNull())
drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}
@@ -0,0 +1,61 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonSpinnerTicks : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load()
{
Origin = Anchor.Centre;
Anchor = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
const float count = 25;
for (float i = 0; i < count; i++)
{
AddInternal(new CircularContainer
{
RelativePositionAxes = Axes.Both,
Masking = true,
CornerRadius = 5,
BorderColour = Color4.White,
BorderThickness = 2f,
Size = new Vector2(30, 5),
Origin = Anchor.Centre,
Position = new Vector2(
0.5f + MathF.Sin(i / count * 2 * MathF.PI) / 2 * 0.75f,
0.5f + MathF.Cos(i / count * 2 * MathF.PI) / 2 * 0.75f
),
Rotation = -i / count * 360 - 120,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Colour4.White.Opacity(0.2f),
Radius = 30,
},
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true,
}
}
});
}
}
}
}
@@ -0,0 +1,67 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class OsuArgonSkinTransformer : SkinTransformer
{
public OsuArgonSkinTransformer(ISkin skin)
: base(skin)
{
}
public override Drawable? GetDrawableComponent(ISkinComponent component)
{
switch (component)
{
case GameplaySkinComponent<HitResult> resultComponent:
return new ArgonJudgementPiece(resultComponent.Component);
case OsuSkinComponent osuComponent:
switch (osuComponent.Component)
{
case OsuSkinComponents.HitCircle:
return new ArgonMainCirclePiece(true);
case OsuSkinComponents.SliderHeadHitCircle:
return new ArgonMainCirclePiece(false);
case OsuSkinComponents.SliderBody:
return new ArgonSliderBody();
case OsuSkinComponents.SliderBall:
return new ArgonSliderBall();
case OsuSkinComponents.SliderFollowCircle:
return new ArgonFollowCircle();
case OsuSkinComponents.SliderScorePoint:
return new ArgonSliderScorePoint();
case OsuSkinComponents.SpinnerBody:
return new ArgonSpinner();
case OsuSkinComponents.ReverseArrow:
return new ArgonReverseArrow();
case OsuSkinComponents.FollowPoint:
return new ArgonFollowPoint();
case OsuSkinComponents.Cursor:
return new ArgonCursor();
case OsuSkinComponents.CursorTrail:
return new ArgonCursorTrail();
}
break;
}
return base.GetDrawableComponent(component);
}
}
}
@@ -10,8 +10,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public abstract class DrawableSliderPath : SmoothPath
{
protected const float BORDER_PORTION = 0.128f;
protected const float GRADIENT_PORTION = 1 - BORDER_PORTION;
public const float BORDER_PORTION = 0.128f;
public const float GRADIENT_PORTION = 1 - BORDER_PORTION;
private const float border_max_size = 8f;
private const float border_min_size = 0f;
@@ -16,9 +16,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public abstract class PlaySliderBody : SnakingSliderBody
{
private IBindable<float> scaleBindable;
protected IBindable<float> ScaleBindable { get; private set; } = null!;
protected IBindable<Color4> AccentColourBindable { get; private set; } = null!;
private IBindable<int> pathVersion;
private IBindable<Color4> accentColour;
[Resolved(CanBeNull = true)]
private OsuRulesetConfigManager config { get; set; }
@@ -30,14 +32,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
var drawableSlider = (DrawableSlider)drawableObject;
scaleBindable = drawableSlider.ScaleBindable.GetBoundCopy();
scaleBindable.BindValueChanged(scale => PathRadius = OsuHitObject.OBJECT_RADIUS * scale.NewValue, true);
ScaleBindable = drawableSlider.ScaleBindable.GetBoundCopy();
ScaleBindable.BindValueChanged(scale => PathRadius = OsuHitObject.OBJECT_RADIUS * scale.NewValue, true);
pathVersion = drawableSlider.PathVersion.GetBoundCopy();
pathVersion.BindValueChanged(_ => Refresh());
accentColour = drawableObject.AccentColour.GetBoundCopy();
accentColour.BindValueChanged(accent => AccentColour = GetBodyAccentColour(skin, accent.NewValue), true);
AccentColourBindable = drawableObject.AccentColour.GetBoundCopy();
AccentColourBindable.BindValueChanged(accent => AccentColour = GetBodyAccentColour(skin, accent.NewValue), true);
config?.BindWith(OsuRulesetSetting.SnakingInSliders, SnakingIn);
config?.BindWith(OsuRulesetSetting.SnakingOutSliders, configSnakingOut);
@@ -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 osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -14,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class RingPiece : CircularContainer
{
public RingPiece()
public RingPiece(float thickness = 9)
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
@@ -22,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Origin = Anchor.Centre;
Masking = true;
BorderThickness = 9; // roughly matches slider borders and makes stacked circles distinctly visible from each other.
BorderThickness = thickness;
BorderColour = Color4.White;
Child = new Box
@@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
var topProvider = source.FindProvider(s => s.GetTexture("spinner-top") != null);
if (topProvider is LegacySkinTransformer transformer && !(transformer.Skin is DefaultLegacySkin))
if (topProvider is ISkinTransformer transformer && !(transformer.Skin is DefaultLegacySkin))
{
AddInternal(ApproachCircle = new Sprite
{
@@ -8,6 +8,7 @@ using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
@@ -186,5 +187,39 @@ namespace osu.Game.Rulesets.Osu.Utils
length * MathF.Sin(angle)
);
}
/// <param name="beatmap">The beatmap hitObject is a part of.</param>
/// <param name="hitObject">The <see cref="OsuHitObject"/> that should be checked.</param>
/// <param name="downbeatsOnly">If true, this method only returns true if hitObject is on a downbeat.
/// If false, it returns true if hitObject is on any beat.</param>
/// <returns>true if hitObject is on a (down-)beat, false otherwise.</returns>
public static bool IsHitObjectOnBeat(OsuBeatmap beatmap, OsuHitObject hitObject, bool downbeatsOnly = false)
{
var timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
double timeSinceTimingPoint = hitObject.StartTime - timingPoint.Time;
double beatLength = timingPoint.BeatLength;
if (downbeatsOnly)
beatLength *= timingPoint.TimeSignature.Numerator;
// Ensure within 1ms of expected location.
return Math.Abs(timeSinceTimingPoint + 1) % beatLength < 2;
}
/// <summary>
/// Generates a random number from a normal distribution using the Box-Muller transform.
/// </summary>
public static float RandomGaussian(Random rng, float mean = 0, float stdDev = 1)
{
// Generate 2 random numbers in the interval (0,1].
// x1 must not be 0 since log(0) = undefined.
double x1 = 1 - rng.NextDouble();
double x2 = 1 - rng.NextDouble();
double stdNormal = Math.Sqrt(-2 * Math.Log(x1)) * Math.Sin(2 * Math.PI * x2);
return mean + stdDev * (float)stdNormal;
}
}
}
@@ -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>
@@ -18,21 +18,15 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
{
MinValue = 0.5f,
MaxValue = 1.5f,
Default = 1f,
Value = 1f,
Precision = 0.1f
};
[SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
public override BindableBool ComboBasedSize { get; } = new BindableBool
{
Default = true,
Value = true
};
public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
public override float DefaultFlashlightSize => 250;
+11 -4
View File
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
@@ -37,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko
{
public class TaikoRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableTaikoRuleset(this, beatmap, mods);
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableTaikoRuleset(this, beatmap, mods);
public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor();
@@ -45,7 +43,16 @@ namespace osu.Game.Rulesets.Taiko
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap, this);
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new TaikoLegacySkinTransformer(skin);
public override ISkin? CreateSkinTransformer(ISkin skin, IBeatmap beatmap)
{
switch (skin)
{
case LegacySkin:
return new TaikoLegacySkinTransformer(skin);
}
return null;
}
public const string SHORT_NAME = "taiko";
@@ -306,7 +306,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
new Color4(128, 255, 128, 255),
new Color4(255, 187, 255, 255),
new Color4(255, 177, 140, 255),
new Color4(100, 100, 100, 100),
new Color4(100, 100, 100, 255), // alpha is specified as 100, but should be ignored.
};
Assert.AreEqual(expectedColors.Length, comboColors.Count);
for (int i = 0; i < expectedColors.Length; i++)
@@ -97,6 +97,25 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
[Test]
public void TestCorrectAnimationStartTime()
{
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("animation-starts-before-alpha.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
Assert.AreEqual(1, background.Elements.Count);
Assert.AreEqual(2000, background.Elements[0].StartTime);
// This property should be used in DrawableStoryboardAnimation as a starting point for animation playback.
Assert.AreEqual(1000, (background.Elements[0] as StoryboardAnimation)?.EarliestTransformTime);
}
}
[Test]
public void TestOutOfOrderStartTimes()
{
@@ -118,17 +118,31 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.IsNull(filterCriteria.BPM.Max);
}
private static readonly object[] length_query_examples =
private static readonly object[] correct_length_query_examples =
{
new object[] { "6ms", TimeSpan.FromMilliseconds(6), TimeSpan.FromMilliseconds(1) },
new object[] { "23s", TimeSpan.FromSeconds(23), TimeSpan.FromSeconds(1) },
new object[] { "9m", TimeSpan.FromMinutes(9), TimeSpan.FromMinutes(1) },
new object[] { "0.25h", TimeSpan.FromHours(0.25), TimeSpan.FromHours(1) },
new object[] { "70", TimeSpan.FromSeconds(70), TimeSpan.FromSeconds(1) },
new object[] { "7m27s", TimeSpan.FromSeconds(447), TimeSpan.FromSeconds(1) },
new object[] { "7:27", TimeSpan.FromSeconds(447), TimeSpan.FromSeconds(1) },
new object[] { "1h2m3s", TimeSpan.FromSeconds(3723), TimeSpan.FromSeconds(1) },
new object[] { "1h2m3.5s", TimeSpan.FromSeconds(3723.5), TimeSpan.FromSeconds(1) },
new object[] { "1:2:3", TimeSpan.FromSeconds(3723), TimeSpan.FromSeconds(1) },
new object[] { "1:02:03", TimeSpan.FromSeconds(3723), TimeSpan.FromSeconds(1) },
new object[] { "6", TimeSpan.FromSeconds(6), TimeSpan.FromSeconds(1) },
new object[] { "6.5", TimeSpan.FromSeconds(6.5), TimeSpan.FromSeconds(1) },
new object[] { "6.5s", TimeSpan.FromSeconds(6.5), TimeSpan.FromSeconds(1) },
new object[] { "6.5m", TimeSpan.FromMinutes(6.5), TimeSpan.FromMinutes(1) },
new object[] { "6h5m", TimeSpan.FromMinutes(365), TimeSpan.FromMinutes(1) },
new object[] { "65m", TimeSpan.FromMinutes(65), TimeSpan.FromMinutes(1) },
new object[] { "90s", TimeSpan.FromSeconds(90), TimeSpan.FromSeconds(1) },
new object[] { "80m20s", TimeSpan.FromSeconds(4820), TimeSpan.FromSeconds(1) },
new object[] { "1h20s", TimeSpan.FromSeconds(3620), TimeSpan.FromSeconds(1) },
};
[Test]
[TestCaseSource(nameof(length_query_examples))]
[TestCaseSource(nameof(correct_length_query_examples))]
public void TestApplyLengthQueries(string lengthQuery, TimeSpan expectedLength, TimeSpan scale)
{
string query = $"length={lengthQuery} time";
@@ -140,6 +154,29 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual(expectedLength.TotalMilliseconds + scale.TotalMilliseconds / 2.0, filterCriteria.Length.Max);
}
private static readonly object[] incorrect_length_query_examples =
{
new object[] { "7.5m27s" },
new object[] { "7m27" },
new object[] { "7m7m7m" },
new object[] { "7m70s" },
new object[] { "5s6m" },
new object[] { "0:" },
new object[] { ":0" },
new object[] { "0:3:" },
new object[] { "3:15.5" },
};
[Test]
[TestCaseSource(nameof(incorrect_length_query_examples))]
public void TestInvalidLengthQueries(string lengthQuery)
{
string query = $"length={lengthQuery} time";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual(false, filterCriteria.Length.HasFilter);
}
[Test]
public void TestApplyDivisorQueries()
{
@@ -154,6 +191,16 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.IsTrue(filterCriteria.BeatDivisor.IsUpperInclusive);
}
[Test]
public void TestPartialStatusMatch()
{
const string query = "status=r";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Min);
Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Max);
}
[Test]
public void TestApplyStatusQueries()
{
@@ -204,31 +204,23 @@ namespace osu.Game.Tests.Online
public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")]
public override BindableNumber<double> InitialRate { get; } = new BindableDouble
public override BindableNumber<double> InitialRate { get; } = new BindableDouble(1.5)
{
MinValue = 1,
MaxValue = 2,
Default = 1.5,
Value = 1.5,
Precision = 0.01,
};
[SettingSource("Final rate", "The speed increase to ramp towards")]
public override BindableNumber<double> FinalRate { get; } = new BindableDouble
public override BindableNumber<double> FinalRate { get; } = new BindableDouble(0.5)
{
MinValue = 0,
MaxValue = 1,
Default = 0.5,
Value = 0.5,
Precision = 0.01,
};
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public override BindableBool AdjustPitch { get; } = new BindableBool
{
Default = true,
Value = true
};
public override BindableBool AdjustPitch { get; } = new BindableBool(true);
}
private class TestModDifficultyAdjust : ModDifficultyAdjust
@@ -124,31 +124,23 @@ namespace osu.Game.Tests.Online
public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")]
public override BindableNumber<double> InitialRate { get; } = new BindableDouble
public override BindableNumber<double> InitialRate { get; } = new BindableDouble(1.5)
{
MinValue = 1,
MaxValue = 2,
Default = 1.5,
Value = 1.5,
Precision = 0.01,
};
[SettingSource("Final rate", "The speed increase to ramp towards")]
public override BindableNumber<double> FinalRate { get; } = new BindableDouble
public override BindableNumber<double> FinalRate { get; } = new BindableDouble(0.5)
{
MinValue = 0,
MaxValue = 1,
Default = 0.5,
Value = 0.5,
Precision = 0.01,
};
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public override BindableBool AdjustPitch { get; } = new BindableBool
{
Default = true,
Value = true
};
public override BindableBool AdjustPitch { get; } = new BindableBool(true);
}
private class TestModEnum : Mod
@@ -91,7 +91,7 @@ namespace osu.Game.Tests.Online
{
AddStep("download beatmap", () => beatmaps.Download(test_db_model));
AddStep("cancel download from notification", () => recentNotification.Close());
AddStep("cancel download from notification", () => recentNotification.Close(true));
AddUntilStep("is removed from download list", () => beatmaps.GetExistingDownload(test_db_model) == null);
AddAssert("is notification cancelled", () => recentNotification.State == ProgressNotificationState.Cancelled);
@@ -0,0 +1,5 @@
[Events]
//Storyboard Layer 0 (Background)
Animation,Background,Centre,"img.jpg",320,240,2,150,LoopForever
S,0,1000,1500,0.08 // animation should start playing from this point in time..
F,0,2000,,0,1 // .. not this point in time
+3 -3
View File
@@ -202,7 +202,7 @@ namespace osu.Game.Tests.Skins.IO
skinManager.CurrentSkinInfo.Value.PerformRead(s =>
{
Assert.IsFalse(s.Protected);
Assert.AreEqual(typeof(DefaultSkin), s.CreateInstance(skinManager).GetType());
Assert.AreEqual(typeof(ArgonSkin), s.CreateInstance(skinManager).GetType());
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
@@ -215,7 +215,7 @@ namespace osu.Game.Tests.Skins.IO
{
Assert.IsFalse(s.Protected);
Assert.AreNotEqual(originalSkinId, s.ID);
Assert.AreEqual(typeof(DefaultSkin), s.CreateInstance(skinManager).GetType());
Assert.AreEqual(typeof(ArgonSkin), s.CreateInstance(skinManager).GetType());
});
return Task.CompletedTask;
@@ -226,7 +226,7 @@ namespace osu.Game.Tests.Skins.IO
{
var skinManager = osu.Dependencies.Get<SkinManager>();
skinManager.CurrentSkinInfo.Value = skinManager.DefaultLegacySkin.SkinInfo;
skinManager.CurrentSkinInfo.Value = skinManager.DefaultClassicSkin.SkinInfo;
skinManager.EnsureMutableSkin();
@@ -29,7 +29,7 @@ namespace osu.Game.Tests.Skins
new Color4(142, 199, 255, 255),
new Color4(255, 128, 128, 255),
new Color4(128, 255, 255, 255),
new Color4(100, 100, 100, 100),
new Color4(100, 100, 100, 255), // alpha is specified as 100, but should be ignored.
};
Assert.AreEqual(expectedColors.Count, comboColors.Count);

Some files were not shown because too many files have changed in this diff Show More