mirror of
https://github.com/osukey/osukey.git
synced 2025-08-02 14:17:06 +09:00
Merge remote-tracking branch 'osulazer/master' into develop
This commit is contained in:
@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"jetbrains.resharper.globaltools": {
|
||||
"version": "2022.1.1",
|
||||
"version": "2022.2.3",
|
||||
"commands": [
|
||||
"jb"
|
||||
]
|
||||
|
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@ -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:
|
||||
@ -82,7 +88,7 @@ jobs:
|
||||
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
|
||||
|
||||
- name: Test
|
||||
run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx"
|
||||
run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" -- NUnit.ConsoleOut=0
|
||||
shell: pwsh
|
||||
|
||||
# Attempt to upload results even if test fails.
|
||||
|
4
.github/workflows/report-nunit.yml
vendored
4
.github/workflows/report-nunit.yml
vendored
@ -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
.github/workflows/sentry-release.yml
vendored
3
.github/workflows/sentry-release.yml
vendored
@ -5,6 +5,9 @@ on:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
sentry_release:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -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.
|
||||
|
@ -9,10 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
|
||||
|
@ -9,10 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||
|
@ -9,10 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
||||
|
@ -9,10 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||
|
@ -51,11 +51,11 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</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.Game.Resources" Version="2022.1005.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1005.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. -->
|
||||
<PackageReference Include="Realm" Version="10.15.1" />
|
||||
<PackageReference Include="Realm" Version="10.17.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -137,12 +137,13 @@ namespace osu.Desktop
|
||||
{
|
||||
base.SetHost(host);
|
||||
|
||||
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
||||
|
||||
var desktopWindow = (SDL2DesktopWindow)host.Window;
|
||||
|
||||
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
||||
if (iconStream != null)
|
||||
desktopWindow.SetIconFromStream(iconStream);
|
||||
|
||||
desktopWindow.CursorState |= CursorState.Hidden;
|
||||
desktopWindow.SetIconFromStream(iconStream);
|
||||
desktopWindow.Title = Name;
|
||||
desktopWindow.DragDrop += f => fileDrop(new[] { f });
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -24,11 +24,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">
|
||||
|
@ -7,9 +7,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
|
||||
<PackageReference Include="nunit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,10 +1,15 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
@ -12,11 +17,11 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[TestFixture]
|
||||
public class TestSceneCatchTouchInput : OsuTestScene
|
||||
{
|
||||
private CatchTouchInputMapper catchTouchInputMapper = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
CatchTouchInputMapper catchTouchInputMapper = null!;
|
||||
|
||||
AddStep("create input overlay", () =>
|
||||
{
|
||||
Child = new CatchInputManager(new CatchRuleset().RulesetInfo)
|
||||
@ -32,12 +37,30 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("show overlay", () => catchTouchInputMapper.Show());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
public void TestWithoutRelax()
|
||||
{
|
||||
AddStep("show overlay", () => catchTouchInputMapper.Show());
|
||||
AddStep("create drawable ruleset without relax mod", () =>
|
||||
{
|
||||
Child = new DrawableCatchRuleset(new CatchRuleset(), new CatchBeatmap(), new List<Mod>());
|
||||
});
|
||||
AddUntilStep("wait for load", () => Child.IsLoaded);
|
||||
AddAssert("check touch input is shown", () => this.ChildrenOfType<CatchTouchInputMapper>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWithRelax()
|
||||
{
|
||||
AddStep("create drawable ruleset with relax mod", () =>
|
||||
{
|
||||
Child = new DrawableCatchRuleset(new CatchRuleset(), new CatchBeatmap(), new List<Mod> { new CatchModRelax() });
|
||||
});
|
||||
AddUntilStep("wait for load", () => Child.IsLoaded);
|
||||
AddAssert("check touch input is not shown", () => !this.ChildrenOfType<CatchTouchInputMapper>().Any());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
49
osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs
Normal file
49
osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs
Normal file
@ -0,0 +1,49 @@
|
||||
// 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.Collections.Generic;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ManiaSpecialColumnTest
|
||||
{
|
||||
[TestCase(new[]
|
||||
{
|
||||
true
|
||||
}, 1)]
|
||||
[TestCase(new[]
|
||||
{
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
}, 4)]
|
||||
[TestCase(new[]
|
||||
{
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
}, 7)]
|
||||
public void Test(IEnumerable<bool> special, int columns)
|
||||
{
|
||||
var definition = new StageDefinition(columns);
|
||||
var results = getResults(definition);
|
||||
Assert.AreEqual(special, results);
|
||||
}
|
||||
|
||||
private IEnumerable<bool> getResults(StageDefinition definition)
|
||||
{
|
||||
for (int i = 0; i < definition.Columns; i++)
|
||||
yield return definition.IsSpecialColumn(i);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
var stageDefinitions = new List<StageDefinition>
|
||||
{
|
||||
new StageDefinition { Columns = 4 },
|
||||
new StageDefinition(4),
|
||||
};
|
||||
|
||||
SetContents(_ => new ManiaPlayfield(stageDefinitions).With(s =>
|
||||
|
97
osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitExplosion.cs
Normal file
97
osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitExplosion.cs
Normal file
@ -0,0 +1,97 @@
|
||||
// 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.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
{
|
||||
public class ArgonHitExplosion : CompositeDrawable, IHitExplosion
|
||||
{
|
||||
public override bool RemoveWhenNotAlive => true;
|
||||
|
||||
[Resolved]
|
||||
private Column column { get; set; } = null!;
|
||||
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
private Container largeFaint = null!;
|
||||
|
||||
private Bindable<Color4> accentColour = null!;
|
||||
|
||||
public ArgonHitExplosion()
|
||||
{
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = ArgonNotePiece.NOTE_HEIGHT;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
largeFaint = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = ArgonNotePiece.CORNER_RADIUS,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Child = new Box
|
||||
{
|
||||
Colour = Color4.White,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
direction.BindTo(scrollingInfo.Direction);
|
||||
direction.BindValueChanged(onDirectionChanged, true);
|
||||
|
||||
accentColour = column.AccentColour.GetBoundCopy();
|
||||
accentColour.BindValueChanged(colour =>
|
||||
{
|
||||
largeFaint.Colour = Interpolation.ValueAt(0.8f, colour.NewValue, Color4.White, 0, 1);
|
||||
|
||||
largeFaint.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = colour.NewValue,
|
||||
Roundness = 40,
|
||||
Radius = 60,
|
||||
};
|
||||
}, true);
|
||||
}
|
||||
|
||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
{
|
||||
if (direction.NewValue == ScrollingDirection.Up)
|
||||
{
|
||||
Anchor = Anchor.TopCentre;
|
||||
Y = ArgonNotePiece.NOTE_HEIGHT / 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Y = -ArgonNotePiece.NOTE_HEIGHT / 2;
|
||||
}
|
||||
}
|
||||
|
||||
public void Animate(JudgementResult result)
|
||||
{
|
||||
this.FadeOutFromOne(PoolableHitExplosion.DURATION, Easing.Out);
|
||||
}
|
||||
}
|
||||
}
|
47
osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitTarget.cs
Normal file
47
osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitTarget.cs
Normal file
@ -0,0 +1,47 @@
|
||||
// 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.UI.Scrolling;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
{
|
||||
public class ArgonHitTarget : CompositeDrawable
|
||||
{
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = ArgonNotePiece.NOTE_HEIGHT;
|
||||
|
||||
Masking = true;
|
||||
CornerRadius = ArgonNotePiece.CORNER_RADIUS;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.3f,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Colour = Color4.White
|
||||
},
|
||||
};
|
||||
|
||||
direction.BindTo(scrollingInfo.Direction);
|
||||
direction.BindValueChanged(onDirectionChanged, true);
|
||||
}
|
||||
|
||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
{
|
||||
Anchor = Origin = direction.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
|
||||
}
|
||||
}
|
||||
}
|
97
osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs
Normal file
97
osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs
Normal file
@ -0,0 +1,97 @@
|
||||
// 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.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents length-wise portion of a hold note.
|
||||
/// </summary>
|
||||
public class ArgonHoldBodyPiece : CompositeDrawable, IHoldNoteBody
|
||||
{
|
||||
protected readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
|
||||
protected readonly IBindable<bool> IsHitting = new Bindable<bool>();
|
||||
|
||||
private Drawable background = null!;
|
||||
private Box foreground = null!;
|
||||
|
||||
public ArgonHoldBodyPiece()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
// Without this, the width of the body will be slightly larger than the head/tail.
|
||||
Masking = true;
|
||||
CornerRadius = ArgonNotePiece.CORNER_RADIUS;
|
||||
Blending = BlendingParameters.Additive;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(DrawableHitObject? drawableObject)
|
||||
{
|
||||
InternalChildren = new[]
|
||||
{
|
||||
background = new Box { RelativeSizeAxes = Axes.Both },
|
||||
foreground = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Alpha = 0,
|
||||
},
|
||||
};
|
||||
|
||||
if (drawableObject != null)
|
||||
{
|
||||
var holdNote = (DrawableHoldNote)drawableObject;
|
||||
|
||||
AccentColour.BindTo(holdNote.AccentColour);
|
||||
IsHitting.BindTo(holdNote.IsHitting);
|
||||
}
|
||||
|
||||
AccentColour.BindValueChanged(colour =>
|
||||
{
|
||||
background.Colour = colour.NewValue.Darken(1.2f);
|
||||
foreground.Colour = colour.NewValue.Opacity(0.2f);
|
||||
}, true);
|
||||
|
||||
IsHitting.BindValueChanged(hitting =>
|
||||
{
|
||||
const float animation_length = 50;
|
||||
|
||||
foreground.ClearTransforms();
|
||||
|
||||
if (hitting.NewValue)
|
||||
{
|
||||
// wait for the next sync point
|
||||
double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
|
||||
|
||||
using (foreground.BeginDelayedSequence(synchronisedOffset))
|
||||
{
|
||||
foreground.FadeTo(1, animation_length).Then()
|
||||
.FadeTo(0.5f, animation_length)
|
||||
.Loop();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreground.FadeOut(animation_length);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Recycle()
|
||||
{
|
||||
foreground.ClearTransforms();
|
||||
foreground.Alpha = 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
// 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.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
{
|
||||
internal class ArgonHoldNoteTailPiece : CompositeDrawable
|
||||
{
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
|
||||
|
||||
private readonly Box colouredBox;
|
||||
private readonly Box shadow;
|
||||
|
||||
public ArgonHoldNoteTailPiece()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = ArgonNotePiece.NOTE_HEIGHT;
|
||||
|
||||
CornerRadius = ArgonNotePiece.CORNER_RADIUS;
|
||||
Masking = true;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
shadow = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.82f,
|
||||
Masking = true,
|
||||
CornerRadius = ArgonNotePiece.CORNER_RADIUS,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
colouredBox = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
}
|
||||
},
|
||||
new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = ArgonNotePiece.CORNER_RADIUS * 2,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(IScrollingInfo scrollingInfo, DrawableHitObject? drawableObject)
|
||||
{
|
||||
direction.BindTo(scrollingInfo.Direction);
|
||||
direction.BindValueChanged(onDirectionChanged, true);
|
||||
|
||||
if (drawableObject != null)
|
||||
{
|
||||
accentColour.BindTo(drawableObject.AccentColour);
|
||||
accentColour.BindValueChanged(onAccentChanged, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
{
|
||||
colouredBox.Anchor = colouredBox.Origin = direction.NewValue == ScrollingDirection.Up
|
||||
? Anchor.TopCentre
|
||||
: Anchor.BottomCentre;
|
||||
}
|
||||
|
||||
private void onAccentChanged(ValueChangedEvent<Color4> accent)
|
||||
{
|
||||
colouredBox.Colour = ColourInfo.GradientVertical(
|
||||
accent.NewValue,
|
||||
accent.NewValue.Darken(0.1f)
|
||||
);
|
||||
|
||||
shadow.Colour = accent.NewValue.Darken(0.5f);
|
||||
}
|
||||
}
|
||||
}
|
193
osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
Normal file
193
osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
Normal file
@ -0,0 +1,193 @@
|
||||
// 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.Shapes;
|
||||
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.Scoring;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.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;
|
||||
Y = 160;
|
||||
}
|
||||
|
||||
[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(10, 0),
|
||||
Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular),
|
||||
},
|
||||
};
|
||||
|
||||
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.4f), 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);
|
||||
}
|
||||
|
||||
public class RingPiece : CircularContainer
|
||||
{
|
||||
public RingPiece(float thickness = 9)
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
Masking = true;
|
||||
BorderThickness = thickness;
|
||||
BorderColour = Color4.White;
|
||||
|
||||
Child = new Box
|
||||
{
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0,
|
||||
RelativeSizeAxes = Axes.Both
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
272
osu.Game.Rulesets.Mania/Skinning/Argon/ArgonKeyArea.cs
Normal file
272
osu.Game.Rulesets.Mania/Skinning/Argon/ArgonKeyArea.cs
Normal file
@ -0,0 +1,272 @@
|
||||
// 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.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
{
|
||||
public class ArgonKeyArea : CompositeDrawable, IKeyBindingHandler<ManiaAction>
|
||||
{
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
private Container directionContainer = null!;
|
||||
private Drawable background = null!;
|
||||
|
||||
private Circle hitTargetLine = null!;
|
||||
|
||||
private Container<Circle> bottomIcon = null!;
|
||||
private CircularContainer topIcon = null!;
|
||||
|
||||
private Bindable<Color4> accentColour = null!;
|
||||
|
||||
[Resolved]
|
||||
private Column column { get; set; } = null!;
|
||||
|
||||
public ArgonKeyArea()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
const float icon_circle_size = 8;
|
||||
const float icon_spacing = 7;
|
||||
const float icon_vertical_offset = -30;
|
||||
|
||||
InternalChild = directionContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
// Ensure the area is tall enough to put the target line in the correct location.
|
||||
// This is to also allow the main background component to overlap the target line
|
||||
// and avoid an inner corner radius being shown below the target line.
|
||||
Height = Stage.HIT_TARGET_POSITION + ArgonNotePiece.CORNER_RADIUS * 2,
|
||||
Children = new[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Masking = true,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CornerRadius = ArgonNotePiece.CORNER_RADIUS,
|
||||
Child = background = new Box
|
||||
{
|
||||
Name = "Key gradient",
|
||||
Alpha = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
},
|
||||
hitTargetLine = new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Colour = OsuColour.Gray(196 / 255f),
|
||||
Height = ArgonNotePiece.CORNER_RADIUS * 2,
|
||||
Masking = true,
|
||||
EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow },
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Name = "Icons",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
bottomIcon = new Container<Circle>
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Y = icon_vertical_offset,
|
||||
Children = new[]
|
||||
{
|
||||
new Circle
|
||||
{
|
||||
Size = new Vector2(icon_circle_size),
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre,
|
||||
EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow },
|
||||
},
|
||||
new Circle
|
||||
{
|
||||
X = -icon_spacing,
|
||||
Y = icon_spacing * 1.2f,
|
||||
Size = new Vector2(icon_circle_size),
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre,
|
||||
EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow },
|
||||
},
|
||||
new Circle
|
||||
{
|
||||
X = icon_spacing,
|
||||
Y = icon_spacing * 1.2f,
|
||||
Size = new Vector2(icon_circle_size),
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre,
|
||||
EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow },
|
||||
},
|
||||
}
|
||||
},
|
||||
topIcon = new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Y = -icon_vertical_offset,
|
||||
Size = new Vector2(22, 14),
|
||||
Masking = true,
|
||||
BorderThickness = 4,
|
||||
BorderColour = Color4.White,
|
||||
EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
direction.BindTo(scrollingInfo.Direction);
|
||||
direction.BindValueChanged(onDirectionChanged, true);
|
||||
|
||||
accentColour = column.AccentColour.GetBoundCopy();
|
||||
accentColour.BindValueChanged(colour =>
|
||||
{
|
||||
background.Colour = colour.NewValue.Darken(0.2f);
|
||||
bottomIcon.Colour = colour.NewValue;
|
||||
},
|
||||
true);
|
||||
|
||||
// Yes, proxy everything.
|
||||
column.TopLevelContainer.Add(CreateProxy());
|
||||
}
|
||||
|
||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
{
|
||||
switch (direction.NewValue)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
directionContainer.Scale = new Vector2(1, -1);
|
||||
directionContainer.Anchor = Anchor.TopLeft;
|
||||
directionContainer.Origin = Anchor.BottomLeft;
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Down:
|
||||
directionContainer.Scale = new Vector2(1, 1);
|
||||
directionContainer.Anchor = Anchor.BottomLeft;
|
||||
directionContainer.Origin = Anchor.BottomLeft;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<ManiaAction> e)
|
||||
{
|
||||
if (e.Action != column.Action.Value) return false;
|
||||
|
||||
const double lighting_fade_in_duration = 70;
|
||||
Color4 lightingColour = getLightingColour();
|
||||
|
||||
background
|
||||
.FlashColour(accentColour.Value.Lighten(0.8f), 200, Easing.OutQuint)
|
||||
.FadeTo(1, lighting_fade_in_duration, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeTo(0.8f, 500);
|
||||
|
||||
hitTargetLine.FadeColour(Color4.White, lighting_fade_in_duration, Easing.OutQuint);
|
||||
hitTargetLine.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = lightingColour.Opacity(0.4f),
|
||||
Radius = 20,
|
||||
}, lighting_fade_in_duration, Easing.OutQuint);
|
||||
|
||||
topIcon.ScaleTo(0.9f, lighting_fade_in_duration, Easing.OutQuint);
|
||||
topIcon.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = lightingColour.Opacity(0.1f),
|
||||
Radius = 20,
|
||||
}, lighting_fade_in_duration, Easing.OutQuint);
|
||||
|
||||
bottomIcon.FadeColour(Color4.White, lighting_fade_in_duration, Easing.OutQuint);
|
||||
|
||||
foreach (var circle in bottomIcon)
|
||||
{
|
||||
circle.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = lightingColour.Opacity(0.2f),
|
||||
Radius = 60,
|
||||
}, lighting_fade_in_duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<ManiaAction> e)
|
||||
{
|
||||
if (e.Action != column.Action.Value) return;
|
||||
|
||||
const double lighting_fade_out_duration = 800;
|
||||
|
||||
Color4 lightingColour = getLightingColour().Opacity(0);
|
||||
|
||||
// background fades out faster than lighting elements to give better definition to the player.
|
||||
background.FadeTo(0.3f, 50, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeOut(lighting_fade_out_duration, Easing.OutQuint);
|
||||
|
||||
topIcon.ScaleTo(1f, 200, Easing.OutQuint);
|
||||
topIcon.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = lightingColour,
|
||||
Radius = 20,
|
||||
}, lighting_fade_out_duration, Easing.OutQuint);
|
||||
|
||||
hitTargetLine.FadeColour(OsuColour.Gray(196 / 255f), lighting_fade_out_duration, Easing.OutQuint);
|
||||
hitTargetLine.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = lightingColour,
|
||||
Radius = 25,
|
||||
}, lighting_fade_out_duration, Easing.OutQuint);
|
||||
|
||||
bottomIcon.FadeColour(accentColour.Value, lighting_fade_out_duration, Easing.OutQuint);
|
||||
|
||||
foreach (var circle in bottomIcon)
|
||||
{
|
||||
circle.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = lightingColour,
|
||||
Radius = 30,
|
||||
}, lighting_fade_out_duration, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
private Color4 getLightingColour() => Interpolation.ValueAt(0.2f, accentColour.Value, Color4.White, 0, 1);
|
||||
}
|
||||
}
|
110
osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs
Normal file
110
osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs
Normal file
@ -0,0 +1,110 @@
|
||||
// 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.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.UI.Scrolling;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
{
|
||||
internal class ArgonNotePiece : CompositeDrawable
|
||||
{
|
||||
public const float NOTE_HEIGHT = 42;
|
||||
|
||||
public const float CORNER_RADIUS = 3.4f;
|
||||
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
|
||||
|
||||
private readonly Box colouredBox;
|
||||
private readonly Box shadow;
|
||||
|
||||
public ArgonNotePiece()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = NOTE_HEIGHT;
|
||||
|
||||
CornerRadius = CORNER_RADIUS;
|
||||
Masking = true;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
shadow = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.82f,
|
||||
Masking = true,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
colouredBox = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
}
|
||||
},
|
||||
new Circle
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = CORNER_RADIUS * 2,
|
||||
},
|
||||
new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Y = 4,
|
||||
Icon = FontAwesome.Solid.AngleDown,
|
||||
Size = new Vector2(20),
|
||||
Scale = new Vector2(1, 0.7f)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(IScrollingInfo scrollingInfo, DrawableHitObject? drawableObject)
|
||||
{
|
||||
direction.BindTo(scrollingInfo.Direction);
|
||||
direction.BindValueChanged(onDirectionChanged, true);
|
||||
|
||||
if (drawableObject != null)
|
||||
{
|
||||
accentColour.BindTo(drawableObject.AccentColour);
|
||||
accentColour.BindValueChanged(onAccentChanged, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
{
|
||||
colouredBox.Anchor = colouredBox.Origin = direction.NewValue == ScrollingDirection.Up
|
||||
? Anchor.TopCentre
|
||||
: Anchor.BottomCentre;
|
||||
}
|
||||
|
||||
private void onAccentChanged(ValueChangedEvent<Color4> accent)
|
||||
{
|
||||
colouredBox.Colour = ColourInfo.GradientVertical(
|
||||
accent.NewValue.Lighten(0.1f),
|
||||
accent.NewValue
|
||||
);
|
||||
|
||||
shadow.Colour = accent.NewValue.Darken(0.5f);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// 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.Containers;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
{
|
||||
public class ArgonStageBackground : CompositeDrawable
|
||||
{
|
||||
public ArgonStageBackground()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
{
|
||||
public class ManiaArgonSkinTransformer : SkinTransformer
|
||||
{
|
||||
private readonly ManiaBeatmap beatmap;
|
||||
|
||||
public ManiaArgonSkinTransformer(ISkin skin, IBeatmap beatmap)
|
||||
: base(skin)
|
||||
{
|
||||
this.beatmap = (ManiaBeatmap)beatmap;
|
||||
}
|
||||
|
||||
public override Drawable? GetDrawableComponent(ISkinComponent component)
|
||||
{
|
||||
switch (component)
|
||||
{
|
||||
case GameplaySkinComponent<HitResult> resultComponent:
|
||||
return new ArgonJudgementPiece(resultComponent.Component);
|
||||
|
||||
case ManiaSkinComponent maniaComponent:
|
||||
// TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries.
|
||||
switch (maniaComponent.Component)
|
||||
{
|
||||
case ManiaSkinComponents.StageBackground:
|
||||
return new ArgonStageBackground();
|
||||
|
||||
case ManiaSkinComponents.ColumnBackground:
|
||||
return new ArgonColumnBackground();
|
||||
|
||||
case ManiaSkinComponents.HoldNoteBody:
|
||||
return new ArgonHoldBodyPiece();
|
||||
|
||||
case ManiaSkinComponents.HoldNoteTail:
|
||||
return new ArgonHoldNoteTailPiece();
|
||||
|
||||
case ManiaSkinComponents.HoldNoteHead:
|
||||
case ManiaSkinComponents.Note:
|
||||
return new ArgonNotePiece();
|
||||
|
||||
case ManiaSkinComponents.HitTarget:
|
||||
return new ArgonHitTarget();
|
||||
|
||||
case ManiaSkinComponents.KeyArea:
|
||||
return new ArgonKeyArea();
|
||||
|
||||
case ManiaSkinComponents.HitExplosion:
|
||||
return new ArgonHitExplosion();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return base.GetDrawableComponent(component);
|
||||
}
|
||||
|
||||
public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
|
||||
{
|
||||
int column = maniaLookup.ColumnIndex ?? 0;
|
||||
var stage = beatmap.GetStageForColumnIndex(column);
|
||||
|
||||
switch (maniaLookup.Lookup)
|
||||
{
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnSpacing:
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(2));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.StagePaddingBottom:
|
||||
case LegacyManiaSkinConfigurationLookups.StagePaddingTop:
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(30));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnWidth:
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(
|
||||
stage.IsSpecialColumn(column) ? 120 : 60
|
||||
));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
|
||||
|
||||
Color4 colour;
|
||||
|
||||
if (stage.IsSpecialColumn(column))
|
||||
colour = new Color4(159, 101, 255, 255);
|
||||
else
|
||||
{
|
||||
switch (column % 8)
|
||||
{
|
||||
default:
|
||||
colour = new Color4(240, 216, 0, 255);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
colour = new Color4(240, 101, 0, 255);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
colour = new Color4(240, 0, 130, 255);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
colour = new Color4(192, 0, 240, 255);
|
||||
break;
|
||||
|
||||
case 4:
|
||||
colour = new Color4(178, 0, 240, 255);
|
||||
break;
|
||||
|
||||
case 5:
|
||||
colour = new Color4(0, 96, 240, 255);
|
||||
break;
|
||||
|
||||
case 6:
|
||||
colour = new Color4(0, 226, 240, 255);
|
||||
break;
|
||||
|
||||
case 7:
|
||||
colour = new Color4(0, 240, 96, 255);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return SkinUtils.As<TValue>(new Bindable<Color4>(colour));
|
||||
}
|
||||
}
|
||||
|
||||
return base.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
// 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.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Default
|
||||
{
|
||||
public class ManiaTrianglesSkinTransformer : SkinTransformer
|
||||
{
|
||||
private readonly ManiaBeatmap beatmap;
|
||||
|
||||
public ManiaTrianglesSkinTransformer(ISkin skin, IBeatmap beatmap)
|
||||
: base(skin)
|
||||
{
|
||||
this.beatmap = (ManiaBeatmap)beatmap;
|
||||
}
|
||||
|
||||
private readonly Color4 colourEven = new Color4(6, 84, 0, 255);
|
||||
private readonly Color4 colourOdd = new Color4(94, 0, 57, 255);
|
||||
private readonly Color4 colourSpecial = new Color4(0, 48, 63, 255);
|
||||
|
||||
public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
|
||||
{
|
||||
switch (maniaLookup.Lookup)
|
||||
{
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
|
||||
int column = maniaLookup.ColumnIndex ?? 0;
|
||||
|
||||
var stage = beatmap.GetStageForColumnIndex(column);
|
||||
|
||||
if (stage.IsSpecialColumn(column))
|
||||
return SkinUtils.As<TValue>(new Bindable<Color4>(colourSpecial));
|
||||
|
||||
int distanceToEdge = Math.Min(column, (stage.Columns - 1) - column);
|
||||
return SkinUtils.As<TValue>(new Bindable<Color4>(distanceToEdge % 2 == 0 ? colourOdd : colourEven));
|
||||
}
|
||||
}
|
||||
|
||||
return base.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
{
|
||||
public class ManiaClassicSkinTransformer : ManiaLegacySkinTransformer
|
||||
{
|
||||
public ManiaClassicSkinTransformer(ISkin skin, IBeatmap beatmap)
|
||||
: base(skin, beatmap)
|
||||
{
|
||||
}
|
||||
|
||||
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
|
||||
{
|
||||
var baseLookup = base.GetConfig<TLookup, TValue>(lookup);
|
||||
|
||||
if (baseLookup != null)
|
||||
return baseLookup;
|
||||
|
||||
// default provisioning.
|
||||
switch (maniaLookup.Lookup)
|
||||
{
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
|
||||
return SkinUtils.As<TValue>(new Bindable<Color4>(Color4.Black));
|
||||
}
|
||||
}
|
||||
|
||||
return base.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
102
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRandom.cs
Normal file
102
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRandom.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 251 B |
BIN
osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor-smoke.png
Normal file
BIN
osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor-smoke.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
@ -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)
|
||||
{
|
||||
|
136
osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs
Normal file
136
osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs
Normal file
@ -0,0 +1,136 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.States;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Testing.Input;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public class TestSceneSmoke : OsuSkinnableTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestSmoking()
|
||||
{
|
||||
addStep("Create short smoke", 2_000);
|
||||
addStep("Create medium smoke", 5_000);
|
||||
addStep("Create long smoke", 10_000);
|
||||
}
|
||||
|
||||
private void addStep(string stepName, double duration)
|
||||
{
|
||||
var smokeContainers = new List<SmokeContainer>();
|
||||
|
||||
AddStep(stepName, () =>
|
||||
{
|
||||
smokeContainers.Clear();
|
||||
SetContents(_ =>
|
||||
{
|
||||
smokeContainers.Add(new TestSmokeContainer
|
||||
{
|
||||
Duration = duration,
|
||||
RelativeSizeAxes = Axes.Both
|
||||
});
|
||||
|
||||
return new SmokingInputManager
|
||||
{
|
||||
Duration = duration,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.95f),
|
||||
Child = smokeContainers[^1],
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
AddUntilStep("Until skinnable expires", () =>
|
||||
{
|
||||
if (smokeContainers.Count == 0)
|
||||
return false;
|
||||
|
||||
Logger.Log("How many: " + smokeContainers.Count);
|
||||
|
||||
foreach (var smokeContainer in smokeContainers)
|
||||
{
|
||||
if (smokeContainer.Children.Count != 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private class SmokingInputManager : ManualInputManager
|
||||
{
|
||||
public double Duration { get; init; }
|
||||
|
||||
private double? startTime;
|
||||
|
||||
public SmokingInputManager()
|
||||
{
|
||||
UseParentInput = false;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
MoveMouseTo(ToScreenSpace(DrawSize / 2));
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
const float spin_angle = 4 * MathF.PI;
|
||||
|
||||
startTime ??= Time.Current;
|
||||
|
||||
float fraction = (float)((Time.Current - startTime) / Duration);
|
||||
|
||||
float angle = fraction * spin_angle;
|
||||
float radius = fraction * Math.Min(DrawSize.X, DrawSize.Y) / 2;
|
||||
|
||||
Vector2 pos = radius * new Vector2(MathF.Cos(angle), MathF.Sin(angle)) + DrawSize / 2;
|
||||
MoveMouseTo(ToScreenSpace(pos));
|
||||
}
|
||||
}
|
||||
|
||||
private class TestSmokeContainer : SmokeContainer
|
||||
{
|
||||
public double Duration { get; init; }
|
||||
|
||||
private bool isPressing;
|
||||
private bool isFinished;
|
||||
|
||||
private double? startTime;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
startTime ??= Time.Current + 0.1;
|
||||
|
||||
if (!isPressing && !isFinished && Time.Current > startTime)
|
||||
{
|
||||
OnPressed(new KeyBindingPressEvent<OsuAction>(new InputState(), OsuAction.Smoke));
|
||||
isPressing = true;
|
||||
isFinished = false;
|
||||
}
|
||||
|
||||
if (isPressing && Time.Current > startTime + Duration)
|
||||
{
|
||||
OnReleased(new KeyBindingReleaseEvent<OsuAction>(new InputState(), OsuAction.Smoke));
|
||||
isPressing = false;
|
||||
isFinished = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
149
osu.Game.Rulesets.Osu.Tests/TestSceneTrianglesSpinnerRotation.cs
Normal file
149
osu.Game.Rulesets.Osu.Tests/TestSceneTrianglesSpinnerRotation.cs
Normal file
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||
<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" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
</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)
|
||||
{
|
||||
|
@ -4,7 +4,6 @@
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
@ -18,13 +17,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,
|
||||
};
|
||||
}
|
||||
|
@ -32,24 +32,16 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
Precision = default_follow_delay,
|
||||
};
|
||||
|
||||
[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;
|
||||
public override float DefaultFlashlightSize => 200;
|
||||
|
||||
private OsuFlashlight flashlight = null!;
|
||||
|
||||
@ -71,6 +63,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
followDelay = modFlashlight.FollowDelay.Value;
|
||||
|
||||
FlashlightSize = new Vector2(0, GetSizeFor(0));
|
||||
FlashlightSmoothness = 1.4f;
|
||||
}
|
||||
|
||||
public void OnSliderTrackingChange(ValueChangedEvent<bool> e)
|
||||
|
@ -4,7 +4,6 @@
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
@ -18,13 +17,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,
|
||||
};
|
||||
}
|
||||
|
@ -7,8 +7,6 @@ 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.Osu.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@ -22,15 +20,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
private PeriodTracker spinnerPeriods = null!;
|
||||
|
||||
[SettingSource(
|
||||
"Hidden at combo",
|
||||
"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,
|
||||
};
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@ -20,6 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
|
||||
public abstract BindableNumber<float> StartScale { get; }
|
||||
|
||||
protected virtual float EndScale => 1;
|
||||
|
@ -4,9 +4,12 @@
|
||||
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;
|
||||
@ -25,6 +28,14 @@ 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 random = null!;
|
||||
@ -50,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
if (shouldStartNewSection(osuBeatmap, positionInfos, i))
|
||||
{
|
||||
sectionOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.0008f);
|
||||
sectionOffset = getRandomOffset(0.0008f);
|
||||
flowDirection = !flowDirection;
|
||||
}
|
||||
|
||||
@ -65,11 +76,11 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
float flowChangeOffset = 0;
|
||||
|
||||
// Offsets only the angle of the current hit object.
|
||||
float oneTimeOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.002f);
|
||||
float oneTimeOffset = getRandomOffset(0.002f);
|
||||
|
||||
if (shouldApplyFlowChange(positionInfos, i))
|
||||
{
|
||||
flowChangeOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.002f);
|
||||
flowChangeOffset = getRandomOffset(0.002f);
|
||||
flowDirection = !flowDirection;
|
||||
}
|
||||
|
||||
@ -86,13 +97,36 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
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 static float getRelativeTargetAngle(float targetDistance, float offset, bool flowDirection)
|
||||
private float getRelativeTargetAngle(float targetDistance, float offset, bool flowDirection)
|
||||
{
|
||||
float angle = (float)(2.16 / (1 + 200 * Math.Exp(0.036 * (targetDistance - 310))) + 0.5 + offset);
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
@ -191,12 +204,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
// todo: temporary / arbitrary, used for lifetime optimisation.
|
||||
this.Delay(800).FadeOut();
|
||||
|
||||
// in the case of an early state change, the fade should be expedited to the current point in time.
|
||||
if (HitStateUpdateTime < HitObject.StartTime)
|
||||
ApproachCircle.FadeOut(50);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
default:
|
||||
ApproachCircle.FadeOut();
|
||||
break;
|
||||
|
||||
case ArmedState.Idle:
|
||||
HitArea.HitAction = null;
|
||||
break;
|
||||
|
@ -6,17 +6,18 @@
|
||||
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 osu.Game.Rulesets.Osu.Scoring;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
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 +35,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 +44,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 +66,32 @@ 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 override void UpdateInitialTransforms()
|
||||
{
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
// Dim should only be applied at a top level, as it will be implicitly applied to nested objects.
|
||||
if (ParentHitObject == null)
|
||||
{
|
||||
// Of note, no one noticed this was missing for years, but it definitely feels like it should still exist.
|
||||
// For now this is applied across all skins, and matches stable.
|
||||
// For simplicity, dim colour is applied to the DrawableHitObject itself.
|
||||
// We may need to make a nested container setup if this even causes a usage conflict (ie. with a mod).
|
||||
this.FadeColour(new Color4(195, 195, 195, 255));
|
||||
using (BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
|
||||
this.FadeColour(Color4.White, 100);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
protected override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;
|
||||
public override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;
|
||||
|
||||
/// <summary>
|
||||
/// Apply a judgement result.
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -80,6 +80,9 @@ namespace osu.Game.Rulesets.Osu
|
||||
LeftButton,
|
||||
|
||||
[Description("Right button")]
|
||||
RightButton
|
||||
RightButton,
|
||||
|
||||
[Description("Smoke")]
|
||||
Smoke,
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
@ -60,6 +59,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
{
|
||||
new KeyBinding(InputKey.Z, OsuAction.LeftButton),
|
||||
new KeyBinding(InputKey.X, OsuAction.RightButton),
|
||||
new KeyBinding(InputKey.C, OsuAction.Smoke),
|
||||
new KeyBinding(InputKey.MouseLeft, OsuAction.LeftButton),
|
||||
new KeyBinding(InputKey.MouseRight, OsuAction.RightButton),
|
||||
};
|
||||
@ -233,13 +233,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()
|
||||
{
|
||||
|
@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
SliderBall,
|
||||
SliderBody,
|
||||
SpinnerBody,
|
||||
CursorSmoke,
|
||||
ApproachCircle,
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
Position = currentFrame.Position;
|
||||
if (currentFrame.MouseLeft) Actions.Add(OsuAction.LeftButton);
|
||||
if (currentFrame.MouseRight) Actions.Add(OsuAction.RightButton);
|
||||
if (currentFrame.Smoke) Actions.Add(OsuAction.Smoke);
|
||||
}
|
||||
|
||||
public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
|
||||
@ -41,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
state |= ReplayButtonState.Left1;
|
||||
if (Actions.Contains(OsuAction.RightButton))
|
||||
state |= ReplayButtonState.Right1;
|
||||
if (Actions.Contains(OsuAction.Smoke))
|
||||
state |= ReplayButtonState.Smoke;
|
||||
|
||||
return new LegacyReplayFrame(Time, Position.X, Position.Y, state);
|
||||
}
|
||||
|
@ -1,20 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Scoring
|
||||
{
|
||||
public class OsuHitWindows : HitWindows
|
||||
{
|
||||
/// <summary>
|
||||
/// osu! ruleset has a fixed miss window regardless of difficulty settings.
|
||||
/// </summary>
|
||||
public const double MISS_WINDOW = 400;
|
||||
|
||||
private static readonly DifficultyRange[] osu_ranges =
|
||||
{
|
||||
new DifficultyRange(HitResult.Great, 80, 50, 20),
|
||||
new DifficultyRange(HitResult.Ok, 140, 100, 60),
|
||||
new DifficultyRange(HitResult.Meh, 200, 150, 100),
|
||||
new DifficultyRange(HitResult.Miss, 400, 400, 400),
|
||||
new DifficultyRange(HitResult.Miss, MISS_WINDOW, MISS_WINDOW, MISS_WINDOW),
|
||||
};
|
||||
|
||||
public override bool IsHitResultAllowed(HitResult result)
|
||||
|
80
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.cs
Normal file
80
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.cs
Normal file
@ -0,0 +1,80 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using 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),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
29
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursorTrail.cs
Normal file
29
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursorTrail.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
71
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs
Normal file
71
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs
Normal file
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
39
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowPoint.cs
Normal file
39
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowPoint.cs
Normal file
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
171
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
Normal file
171
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
226
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
Normal file
226
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
Normal file
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs
Normal file
54
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
109
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
Normal file
109
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
40
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBody.cs
Normal file
40
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBody.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
146
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinner.cs
Normal file
146
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinner.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
247
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs
Normal file
247
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
61
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerTicks.cs
Normal file
61
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerTicks.cs
Normal file
@ -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,68 @@
|
||||
// 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:
|
||||
// TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries.
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
// 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.Textures;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
{
|
||||
public class DefaultSmokeSegment : SmokeSegment
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(TextureStore textures)
|
||||
{
|
||||
// ISkinSource doesn't currently fallback to global textures.
|
||||
// We might want to change this in the future if the intention is to allow the user to skin this as per legacy skins.
|
||||
Texture = textures.Get("Gameplay/osu/cursor-smoke");
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
19
osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySmokeSegment.cs
Normal file
19
osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySmokeSegment.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// 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.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
public class LegacySmokeSegment : SmokeSegment
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Texture = skin.GetTexture("cursor-smoke");
|
||||
}
|
||||
}
|
||||
}
|
@ -106,6 +106,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
return null;
|
||||
|
||||
case OsuSkinComponents.CursorSmoke:
|
||||
if (GetTexture("cursor-smoke") != null)
|
||||
return new LegacySmokeSegment();
|
||||
|
||||
return null;
|
||||
|
||||
case OsuSkinComponents.HitCircleText:
|
||||
if (!this.HasFont(LegacyFont.HitCircle))
|
||||
return null;
|
||||
|
366
osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
Normal file
366
osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
Normal file
@ -0,0 +1,366 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
public abstract class SmokeSegment : Drawable, ITexturedShaderDrawable
|
||||
{
|
||||
private const int max_point_count = 18_000;
|
||||
|
||||
// fade anim values
|
||||
private const double initial_fade_out_duration = 4000;
|
||||
|
||||
private const double re_fade_in_speed = 3;
|
||||
private const double re_fade_in_duration = 50;
|
||||
|
||||
private const double final_fade_out_speed = 2;
|
||||
private const double final_fade_out_duration = 8000;
|
||||
|
||||
private const float initial_alpha = 0.6f;
|
||||
private const float re_fade_in_alpha = 1f;
|
||||
|
||||
private readonly int rotationSeed = RNG.Next();
|
||||
|
||||
// scale anim values
|
||||
private const double scale_duration = 1200;
|
||||
|
||||
private const float initial_scale = 0.65f;
|
||||
private const float final_scale = 1f;
|
||||
|
||||
// rotation anim values
|
||||
private const double rotation_duration = 500;
|
||||
|
||||
private const float max_rotation = 0.25f;
|
||||
|
||||
public IShader? TextureShader { get; private set; }
|
||||
public IShader? RoundedTextureShader { get; private set; }
|
||||
|
||||
protected Texture? Texture { get; set; }
|
||||
|
||||
private float radius => Texture?.DisplayWidth * 0.165f ?? 3;
|
||||
|
||||
protected readonly List<SmokePoint> SmokePoints = new List<SmokePoint>();
|
||||
|
||||
private float pointInterval => radius * 7f / 8;
|
||||
|
||||
private double smokeStartTime { get; set; } = double.MinValue;
|
||||
|
||||
private double smokeEndTime { get; set; } = double.MaxValue;
|
||||
|
||||
private float totalDistance;
|
||||
private Vector2? lastPosition;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ShaderManager shaders)
|
||||
{
|
||||
RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
|
||||
TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
LifetimeStart = smokeStartTime = Time.Current;
|
||||
|
||||
totalDistance = pointInterval;
|
||||
}
|
||||
|
||||
private Vector2 nextPointDirection()
|
||||
{
|
||||
float angle = RNG.NextSingle(0, 2 * MathF.PI);
|
||||
return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
|
||||
}
|
||||
|
||||
public void AddPosition(Vector2 position, double time)
|
||||
{
|
||||
lastPosition ??= position;
|
||||
|
||||
float delta = (position - (Vector2)lastPosition).LengthFast;
|
||||
totalDistance += delta;
|
||||
int count = (int)(totalDistance / pointInterval);
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
Vector2 increment = position - (Vector2)lastPosition;
|
||||
increment.NormalizeFast();
|
||||
|
||||
Vector2 pointPos = (pointInterval - (totalDistance - delta)) * increment + (Vector2)lastPosition;
|
||||
increment *= pointInterval;
|
||||
|
||||
if (SmokePoints.Count > 0 && SmokePoints[^1].Time > time)
|
||||
{
|
||||
int index = ~SmokePoints.BinarySearch(new SmokePoint { Time = time }, new SmokePoint.UpperBoundComparer());
|
||||
SmokePoints.RemoveRange(index, SmokePoints.Count - index);
|
||||
}
|
||||
|
||||
totalDistance %= pointInterval;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
SmokePoints.Add(new SmokePoint
|
||||
{
|
||||
Position = pointPos,
|
||||
Time = time,
|
||||
Direction = nextPointDirection(),
|
||||
});
|
||||
|
||||
pointPos += increment;
|
||||
}
|
||||
|
||||
Invalidate(Invalidation.DrawNode);
|
||||
}
|
||||
|
||||
lastPosition = position;
|
||||
|
||||
if (SmokePoints.Count >= max_point_count)
|
||||
FinishDrawing(time);
|
||||
}
|
||||
|
||||
public void FinishDrawing(double time)
|
||||
{
|
||||
smokeEndTime = time;
|
||||
|
||||
double initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, smokeEndTime - smokeStartTime);
|
||||
LifetimeEnd = smokeEndTime + final_fade_out_duration + initialFadeOutDurationTrunc / re_fade_in_speed + initialFadeOutDurationTrunc / final_fade_out_speed;
|
||||
}
|
||||
|
||||
protected override DrawNode CreateDrawNode() => new SmokeDrawNode(this);
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Invalidate(Invalidation.DrawNode);
|
||||
}
|
||||
|
||||
protected struct SmokePoint
|
||||
{
|
||||
public Vector2 Position;
|
||||
public double Time;
|
||||
public Vector2 Direction;
|
||||
|
||||
public struct UpperBoundComparer : IComparer<SmokePoint>
|
||||
{
|
||||
public int Compare(SmokePoint x, SmokePoint target)
|
||||
{
|
||||
// By returning -1 when the target value is equal to x, guarantees that the
|
||||
// element at BinarySearch's returned index will always be the first element
|
||||
// larger. Since 0 is never returned, the target is never "found", so the return
|
||||
// value will be the index's complement.
|
||||
|
||||
return x.Time > target.Time ? 1 : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected class SmokeDrawNode : TexturedShaderDrawNode
|
||||
{
|
||||
protected new SmokeSegment Source => (SmokeSegment)base.Source;
|
||||
|
||||
protected double SmokeStartTime { get; private set; }
|
||||
protected double SmokeEndTime { get; private set; }
|
||||
protected double CurrentTime { get; private set; }
|
||||
|
||||
private readonly List<SmokePoint> points = new List<SmokePoint>();
|
||||
private IVertexBatch<TexturedVertex2D>? quadBatch;
|
||||
private float radius;
|
||||
private Vector2 drawSize;
|
||||
private Texture? texture;
|
||||
|
||||
// anim calculation vars (color, scale, direction)
|
||||
private double initialFadeOutDurationTrunc;
|
||||
private double firstVisiblePointTime;
|
||||
|
||||
private double initialFadeOutTime;
|
||||
private double reFadeInTime;
|
||||
private double finalFadeOutTime;
|
||||
|
||||
private Random rotationRNG = new Random();
|
||||
|
||||
public SmokeDrawNode(ITexturedShaderDrawable source)
|
||||
: base(source)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ApplyState()
|
||||
{
|
||||
base.ApplyState();
|
||||
|
||||
points.Clear();
|
||||
points.AddRange(Source.SmokePoints);
|
||||
|
||||
radius = Source.radius;
|
||||
drawSize = Source.DrawSize;
|
||||
texture = Source.Texture;
|
||||
|
||||
SmokeStartTime = Source.smokeStartTime;
|
||||
SmokeEndTime = Source.smokeEndTime;
|
||||
CurrentTime = Source.Clock.CurrentTime;
|
||||
|
||||
rotationRNG = new Random(Source.rotationSeed);
|
||||
|
||||
initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, SmokeEndTime - SmokeStartTime);
|
||||
firstVisiblePointTime = SmokeEndTime - initialFadeOutDurationTrunc;
|
||||
|
||||
initialFadeOutTime = CurrentTime;
|
||||
reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / re_fade_in_speed);
|
||||
finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / final_fade_out_speed);
|
||||
}
|
||||
|
||||
public sealed override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
|
||||
if (points.Count == 0)
|
||||
return;
|
||||
|
||||
quadBatch ??= renderer.CreateQuadBatch<TexturedVertex2D>(max_point_count / 10, 10);
|
||||
texture ??= renderer.WhitePixel;
|
||||
RectangleF textureRect = texture.GetTextureRect();
|
||||
|
||||
var shader = GetAppropriateShader(renderer);
|
||||
|
||||
renderer.SetBlend(BlendingParameters.Additive);
|
||||
renderer.PushLocalMatrix(DrawInfo.Matrix);
|
||||
|
||||
shader.Bind();
|
||||
texture.Bind();
|
||||
|
||||
foreach (var point in points)
|
||||
drawPointQuad(point, textureRect);
|
||||
|
||||
shader.Unbind();
|
||||
renderer.PopLocalMatrix();
|
||||
}
|
||||
|
||||
protected Color4 ColourAtPosition(Vector2 localPos) => DrawColourInfo.Colour.HasSingleColour
|
||||
? ((SRGBColour)DrawColourInfo.Colour).Linear
|
||||
: DrawColourInfo.Colour.Interpolate(Vector2.Divide(localPos, drawSize)).Linear;
|
||||
|
||||
protected virtual Color4 PointColour(SmokePoint point)
|
||||
{
|
||||
var color = Color4.White;
|
||||
|
||||
double timeDoingInitialFadeOut = Math.Min(initialFadeOutTime, SmokeEndTime) - point.Time;
|
||||
|
||||
if (timeDoingInitialFadeOut > 0)
|
||||
{
|
||||
float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1);
|
||||
color.A = (1 - fraction) * initial_alpha;
|
||||
}
|
||||
|
||||
if (color.A > 0)
|
||||
{
|
||||
double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed;
|
||||
double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed;
|
||||
|
||||
if (timeDoingFinalFadeOut > 0)
|
||||
{
|
||||
float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1);
|
||||
fraction = MathF.Pow(fraction, 5);
|
||||
color.A = (1 - fraction) * re_fade_in_alpha;
|
||||
}
|
||||
else if (timeDoingReFadeIn > 0)
|
||||
{
|
||||
float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1);
|
||||
fraction = 1 - MathF.Pow(1 - fraction, 5);
|
||||
color.A = fraction * (re_fade_in_alpha - color.A) + color.A;
|
||||
}
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
protected virtual float PointScale(SmokePoint point)
|
||||
{
|
||||
double timeDoingScale = CurrentTime - point.Time;
|
||||
float fraction = Math.Clamp((float)(timeDoingScale / scale_duration), 0, 1);
|
||||
fraction = 1 - MathF.Pow(1 - fraction, 5);
|
||||
return fraction * (final_scale - initial_scale) + initial_scale;
|
||||
}
|
||||
|
||||
protected virtual Vector2 PointDirection(SmokePoint point)
|
||||
{
|
||||
float initialAngle = MathF.Atan2(point.Direction.Y, point.Direction.X);
|
||||
float finalAngle = initialAngle + nextRotation();
|
||||
|
||||
double timeDoingRotation = CurrentTime - point.Time;
|
||||
float fraction = Math.Clamp((float)(timeDoingRotation / rotation_duration), 0, 1);
|
||||
fraction = 1 - MathF.Pow(1 - fraction, 5);
|
||||
float angle = fraction * (finalAngle - initialAngle) + initialAngle;
|
||||
|
||||
return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
|
||||
}
|
||||
|
||||
private float nextRotation() => max_rotation * ((float)rotationRNG.NextDouble() * 2 - 1);
|
||||
|
||||
private void drawPointQuad(SmokePoint point, RectangleF textureRect)
|
||||
{
|
||||
Debug.Assert(quadBatch != null);
|
||||
|
||||
var colour = PointColour(point);
|
||||
float scale = PointScale(point);
|
||||
var dir = PointDirection(point);
|
||||
var ortho = dir.PerpendicularLeft;
|
||||
|
||||
if (colour.A == 0 || scale == 0)
|
||||
return;
|
||||
|
||||
var localTopLeft = point.Position + (radius * scale * (-ortho - dir));
|
||||
var localTopRight = point.Position + (radius * scale * (-ortho + dir));
|
||||
var localBotLeft = point.Position + (radius * scale * (ortho - dir));
|
||||
var localBotRight = point.Position + (radius * scale * (ortho + dir));
|
||||
|
||||
quadBatch.Add(new TexturedVertex2D
|
||||
{
|
||||
Position = localTopLeft,
|
||||
TexturePosition = textureRect.TopLeft,
|
||||
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopLeft), colour),
|
||||
});
|
||||
quadBatch.Add(new TexturedVertex2D
|
||||
{
|
||||
Position = localTopRight,
|
||||
TexturePosition = textureRect.TopRight,
|
||||
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopRight), colour),
|
||||
});
|
||||
quadBatch.Add(new TexturedVertex2D
|
||||
{
|
||||
Position = localBotRight,
|
||||
TexturePosition = textureRect.BottomRight,
|
||||
Colour = Color4Extensions.Multiply(ColourAtPosition(localBotRight), colour),
|
||||
});
|
||||
quadBatch.Add(new TexturedVertex2D
|
||||
{
|
||||
Position = localBotLeft,
|
||||
TexturePosition = textureRect.BottomLeft,
|
||||
Colour = Color4Extensions.Multiply(ColourAtPosition(localBotLeft), colour),
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
quadBatch?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -54,6 +54,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
|
||||
new SmokeContainer { RelativeSizeAxes = Axes.Both },
|
||||
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
|
||||
FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
|
||||
judgementLayer = new JudgementContainer<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },
|
||||
|
77
osu.Game.Rulesets.Osu/UI/SmokeContainer.cs
Normal file
77
osu.Game.Rulesets.Osu/UI/SmokeContainer.cs
Normal file
@ -0,0 +1,77 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages smoke trails generated from user input.
|
||||
/// </summary>
|
||||
public class SmokeContainer : Container, IRequireHighFrequencyMousePosition, IKeyBindingHandler<OsuAction>
|
||||
{
|
||||
private SmokeSkinnableDrawable? currentSegmentSkinnable;
|
||||
|
||||
private Vector2 lastMousePosition;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 _) => true;
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
||||
{
|
||||
if (e.Action == OsuAction.Smoke)
|
||||
{
|
||||
AddInternal(currentSegmentSkinnable = new SmokeSkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.CursorSmoke), _ => new DefaultSmokeSegment()));
|
||||
|
||||
// Add initial position immediately.
|
||||
addPosition();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
|
||||
{
|
||||
if (e.Action == OsuAction.Smoke)
|
||||
{
|
||||
if (currentSegmentSkinnable?.Drawable is SmokeSegment segment)
|
||||
{
|
||||
segment.FinishDrawing(Time.Current);
|
||||
currentSegmentSkinnable = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
lastMousePosition = e.MousePosition;
|
||||
addPosition();
|
||||
|
||||
return base.OnMouseMove(e);
|
||||
}
|
||||
|
||||
private void addPosition() => (currentSegmentSkinnable?.Drawable as SmokeSegment)?.AddPosition(lastMousePosition, Time.Current);
|
||||
|
||||
private class SmokeSkinnableDrawable : SkinnableDrawable
|
||||
{
|
||||
public override bool RemoveWhenNotAlive => true;
|
||||
|
||||
public override double LifetimeStart => Drawable.LifetimeStart;
|
||||
public override double LifetimeEnd => Drawable.LifetimeEnd;
|
||||
|
||||
public SmokeSkinnableDrawable(ISkinComponent component, Func<ISkinComponent, Drawable>? defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling)
|
||||
: base(component, defaultImplementation, confineMode)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user