mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 18:23:04 +08:00
Merge branch 'master' into catch-hyperdash-stable-sort
This commit is contained in:
commit
bbe9d95b3d
39
README.md
39
README.md
@ -12,40 +12,43 @@
|
|||||||
|
|
||||||
A free-to-win rhythm game. Rhythm is just a *click* away!
|
A free-to-win rhythm game. Rhythm is just a *click* away!
|
||||||
|
|
||||||
The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge.
|
This is the future – and final – iteration of the [osu!](https://osu.ppy.sh) game client which marks the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge.
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
This project is under constant development, but we aim to keep things in a stable state. Users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve to the point of eventually replacing the existing stable client as an update.
|
This project is under constant development, but we do our best to keep things in a stable state. Players are encouraged to install from a release alongside their stable *osu!* client. This project will continue to evolve until we eventually reach the point where most users prefer it over the previous "osu!stable" release.
|
||||||
|
|
||||||
**IMPORTANT:** Gameplay mechanics (and other features which you may have come to know and love) are in a constant state of flux. Game balance and final quality-of-life passes come at the end of development, preceded by experimentation and changes which may potentially **reduce playability or usability**. This is done in order to allow us to move forward as developers and designers more efficiently. If this offends you, please consider sticking to a [stable release](https://osu.ppy.sh/home/download) of osu!. We are not yet open to heated discussion over game mechanics and will not be using github as a forum for such discussions just yet.
|
A few resources are available as starting points to getting involved and understanding the project:
|
||||||
|
|
||||||
We are accepting bug reports (please report with as much detail as possible and follow the existing issue templates). Feature requests are also welcome, but understand that our focus is on completing the game to feature parity before adding new features. A few resources are available as starting points to getting involved and understanding the project:
|
|
||||||
|
|
||||||
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
|
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
|
||||||
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
|
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
|
||||||
|
- Track our current efforts [towards full "ranked play" support](https://github.com/orgs/ppy/projects/13?query=is%3Aopen+sort%3Aupdated-desc).
|
||||||
|
|
||||||
## Running osu!
|
## Running osu!
|
||||||
|
|
||||||
If you are looking to install or test osu! without setting up a development environment, you can consume our [releases](https://github.com/ppy/osu/releases). You can also generally download a version for your current device from the [osu! site](https://osu.ppy.sh/home/download). Failing that, you may use the links below to download the latest version for your operating system of choice:
|
If you are just looking to give the game a whirl, you can grab the latest release for your platform:
|
||||||
|
|
||||||
**Latest release:**
|
### Latest release:
|
||||||
|
|
||||||
| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 13.4+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) |
|
| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 13.4+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) |
|
||||||
| ------------- | ------------- | ------------- | ------------- | ------------- |
|
| ------------- | ------------- | ------------- | ------------- | ------------- |
|
||||||
|
|
||||||
- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.
|
You can also generally download a version for your current device from the [osu! site](https://osu.ppy.sh/home/download).
|
||||||
|
|
||||||
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
|
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
|
||||||
|
|
||||||
|
**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores in early 2024.
|
||||||
|
|
||||||
## Developing a custom ruleset
|
## Developing a custom ruleset
|
||||||
|
|
||||||
osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates).
|
osu! is designed to allow user-created gameplay variations, called "rulesets". Building one of these allows a developer to harness the power of the osu! beatmap library, game engine, and general UX for a new style of gameplay. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates).
|
||||||
|
|
||||||
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/discussions/13096).
|
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/discussions/13096).
|
||||||
|
|
||||||
## Developing osu!
|
## Developing osu!
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
Please make sure you have the following prerequisites:
|
Please make sure you have the following prerequisites:
|
||||||
|
|
||||||
- A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed.
|
- A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed.
|
||||||
@ -69,9 +72,19 @@ git pull
|
|||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing).
|
#### From an IDE
|
||||||
|
|
||||||
- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln`. This will allow access to template run configurations.
|
You should load the solution via one of the platform-specific `.slnf` files, rather than the main `.sln`. This will reduce dependencies and hide platforms that you don't care about. Valid `.slnf` files are:
|
||||||
|
|
||||||
|
- `osu.Desktop.slnf` (most common)
|
||||||
|
- `osu.Android.slnf`
|
||||||
|
- `osu.iOS.slnf`
|
||||||
|
|
||||||
|
Run configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `osu! (Tests)` project/configuration. More information on this is provided [below](#contributing).
|
||||||
|
|
||||||
|
To build for mobile platforms, you will likely need to run `sudo dotnet workload restore` if you haven't done so previously. This will install Android/iOS tooling required to complete the build.
|
||||||
|
|
||||||
|
#### From CLI
|
||||||
|
|
||||||
You can also build and run *osu!* from the command-line with a single command:
|
You can also build and run *osu!* from the command-line with a single command:
|
||||||
|
|
||||||
@ -79,12 +92,10 @@ You can also build and run *osu!* from the command-line with a single command:
|
|||||||
dotnet run --project osu.Desktop
|
dotnet run --project osu.Desktop
|
||||||
```
|
```
|
||||||
|
|
||||||
If you are not interested in debugging *osu!*, you can add `-c Release` to gain performance. In this case, you must replace `Debug` with `Release` in any commands mentioned in this document.
|
When running locally to do any kind of performance testing, make sure to add `-c Release` to the build command, as the overhead of running with the default `Debug` configuration can be large (especially when testing with local framework modifications as below).
|
||||||
|
|
||||||
If the build fails, try to restore NuGet packages with `dotnet restore`.
|
If the build fails, try to restore NuGet packages with `dotnet restore`.
|
||||||
|
|
||||||
_Due to a historical feature gap between .NET Core and Xamarin, running `dotnet` CLI from the root directory will not work for most commands. This can be resolved by specifying a target `.csproj` or the helper project at `build/Desktop.proj`. Configurations have been provided to work around this issue for all supported IDEs mentioned above._
|
|
||||||
|
|
||||||
### Testing with resource/framework modifications
|
### Testing with resource/framework modifications
|
||||||
|
|
||||||
Sometimes it may be necessary to cross-test changes in [osu-resources](https://github.com/ppy/osu-resources) or [osu-framework](https://github.com/ppy/osu-framework). This can be quickly achieved using included commands:
|
Sometimes it may be necessary to cross-test changes in [osu-resources](https://github.com/ppy/osu-resources) or [osu-framework](https://github.com/ppy/osu-framework). This can be quickly achieved using included commands:
|
||||||
|
@ -9,9 +9,9 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
|
||||||
|
@ -9,9 +9,9 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||||
|
@ -9,9 +9,9 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
||||||
|
@ -9,9 +9,9 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.914.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.922.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
|
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
|
||||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||||
<PackageReference Include="System.IO.Packaging" Version="7.0.0" />
|
<PackageReference Include="System.IO.Packaging" Version="7.0.0" />
|
||||||
<PackageReference Include="DiscordRichPresence" Version="1.1.4.20" />
|
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Resources">
|
<ItemGroup Label="Resources">
|
||||||
<EmbeddedResource Include="lazer.ico" />
|
<EmbeddedResource Include="lazer.ico" />
|
||||||
|
@ -7,9 +7,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.4" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.13.8" />
|
||||||
<PackageReference Include="nunit" Version="3.13.3" />
|
<PackageReference Include="nunit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
157
osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs
Normal file
157
osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Catch.Judgements;
|
||||||
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
using osu.Game.Rulesets.Catch.Scoring;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Tests.Visual.Gameplay;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public partial class TestSceneScoring : ScoringTestScene
|
||||||
|
{
|
||||||
|
public TestSceneScoring()
|
||||||
|
: base(supportsNonPerfectJudgements: false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bindable<double> scoreMultiplier { get; } = new BindableDouble
|
||||||
|
{
|
||||||
|
Default = 4,
|
||||||
|
Value = 4
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override IBeatmap CreateBeatmap(int maxCombo)
|
||||||
|
{
|
||||||
|
var beatmap = new CatchBeatmap();
|
||||||
|
for (int i = 0; i < maxCombo; ++i)
|
||||||
|
beatmap.HitObjects.Add(new Fruit());
|
||||||
|
return beatmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1 { ScoreMultiplier = { BindTarget = scoreMultiplier } };
|
||||||
|
|
||||||
|
protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo);
|
||||||
|
|
||||||
|
protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new CatchProcessorBasedScoringAlgorithm(beatmap, mode);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBasicScenarios()
|
||||||
|
{
|
||||||
|
AddStep("set up score multiplier", () =>
|
||||||
|
{
|
||||||
|
scoreMultiplier.BindValueChanged(_ => Rerun());
|
||||||
|
});
|
||||||
|
AddStep("set max combo to 100", () => MaxCombo.Value = 100);
|
||||||
|
AddStep("set perfect score", () =>
|
||||||
|
{
|
||||||
|
NonPerfectLocations.Clear();
|
||||||
|
MissLocations.Clear();
|
||||||
|
});
|
||||||
|
AddStep("set score with misses", () =>
|
||||||
|
{
|
||||||
|
NonPerfectLocations.Clear();
|
||||||
|
MissLocations.Clear();
|
||||||
|
MissLocations.AddRange(new[] { 24d, 49 });
|
||||||
|
});
|
||||||
|
AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int base_great = 300;
|
||||||
|
|
||||||
|
private class ScoreV1 : IScoringAlgorithm
|
||||||
|
{
|
||||||
|
private int currentCombo;
|
||||||
|
|
||||||
|
public BindableDouble ScoreMultiplier { get; } = new BindableDouble();
|
||||||
|
|
||||||
|
public void ApplyHit() => applyHitV1(base_great);
|
||||||
|
|
||||||
|
public void ApplyNonPerfect() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements.");
|
||||||
|
|
||||||
|
public void ApplyMiss() => applyHitV1(0);
|
||||||
|
|
||||||
|
private void applyHitV1(int baseScore)
|
||||||
|
{
|
||||||
|
if (baseScore == 0)
|
||||||
|
{
|
||||||
|
currentCombo = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TotalScore += baseScore;
|
||||||
|
|
||||||
|
// combo multiplier
|
||||||
|
// ReSharper disable once PossibleLossOfFraction
|
||||||
|
TotalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * ScoreMultiplier.Value));
|
||||||
|
|
||||||
|
currentCombo++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long TotalScore { get; private set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ScoreV2 : IScoringAlgorithm
|
||||||
|
{
|
||||||
|
private int currentCombo;
|
||||||
|
private double comboPortion;
|
||||||
|
|
||||||
|
private readonly double comboPortionMax;
|
||||||
|
|
||||||
|
private const double combo_base = 4;
|
||||||
|
private const int combo_cap = 200;
|
||||||
|
|
||||||
|
public ScoreV2(int maxCombo)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < maxCombo; i++)
|
||||||
|
ApplyHit();
|
||||||
|
|
||||||
|
comboPortionMax = comboPortion;
|
||||||
|
|
||||||
|
currentCombo = 0;
|
||||||
|
comboPortion = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyHit() => applyHitV2(base_great);
|
||||||
|
|
||||||
|
public void ApplyNonPerfect() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements.");
|
||||||
|
|
||||||
|
private void applyHitV2(int baseScore)
|
||||||
|
{
|
||||||
|
comboPortion += baseScore * Math.Min(Math.Max(0.5, Math.Log(++currentCombo, combo_base)), Math.Log(combo_cap, combo_base));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyMiss()
|
||||||
|
{
|
||||||
|
currentCombo = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long TotalScore
|
||||||
|
=> (int)Math.Round(1000000 * comboPortion / comboPortionMax); // vast simplification, as we're not doing ticks here.
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CatchProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm
|
||||||
|
{
|
||||||
|
public CatchProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode)
|
||||||
|
: base(beatmap, mode)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor();
|
||||||
|
|
||||||
|
protected override JudgementResult CreatePerfectJudgementResult() => new CatchJudgementResult(new Fruit(), new CatchJudgement()) { Type = HitResult.Great };
|
||||||
|
|
||||||
|
protected override JudgementResult CreateNonPerfectJudgementResult() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements.");
|
||||||
|
|
||||||
|
protected override JudgementResult CreateMissJudgementResult() => new CatchJudgementResult(new Fruit(), new CatchJudgement()) { Type = HitResult.Miss };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
@ -41,7 +41,6 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
|||||||
X = xPositionData?.X ?? 0,
|
X = xPositionData?.X ?? 0,
|
||||||
NewCombo = comboData?.NewCombo ?? false,
|
NewCombo = comboData?.NewCombo ?? false,
|
||||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||||
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
|
|
||||||
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
||||||
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
|
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
|
||||||
}.Yield();
|
}.Yield();
|
||||||
|
@ -25,6 +25,7 @@ using osu.Game.Rulesets.Edit;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Replays.Types;
|
using osu.Game.Rulesets.Replays.Types;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.Scoring.Legacy;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Ranking.Statistics;
|
using osu.Game.Screens.Ranking.Statistics;
|
||||||
|
@ -26,12 +26,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
|
|
||||||
public override int Version => 20220701;
|
public override int Version => 20220701;
|
||||||
|
|
||||||
private readonly IWorkingBeatmap workingBeatmap;
|
|
||||||
|
|
||||||
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
workingBeatmap = beatmap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
@ -50,15 +47,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)),
|
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ComputeLegacyScoringValues)
|
|
||||||
{
|
|
||||||
CatchLegacyScoreSimulator sv1Simulator = new CatchLegacyScoreSimulator();
|
|
||||||
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
|
||||||
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
|
||||||
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
|
||||||
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,33 +2,26 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.Scoring.Legacy;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||||
{
|
{
|
||||||
internal class CatchLegacyScoreSimulator : ILegacyScoreSimulator
|
internal class CatchLegacyScoreSimulator : ILegacyScoreSimulator
|
||||||
{
|
{
|
||||||
public int AccuracyScore { get; private set; }
|
|
||||||
|
|
||||||
public int ComboScore { get; private set; }
|
|
||||||
|
|
||||||
public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore;
|
|
||||||
|
|
||||||
private int legacyBonusScore;
|
private int legacyBonusScore;
|
||||||
private int modernBonusScore;
|
private int standardisedBonusScore;
|
||||||
private int combo;
|
private int combo;
|
||||||
|
|
||||||
private double scoreMultiplier;
|
private double scoreMultiplier;
|
||||||
|
|
||||||
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap)
|
||||||
{
|
{
|
||||||
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
||||||
|
|
||||||
@ -70,13 +63,19 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
+ baseBeatmap.Difficulty.CircleSize
|
+ baseBeatmap.Difficulty.CircleSize
|
||||||
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||||
|
|
||||||
scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
scoreMultiplier = difficultyPeppyStars;
|
||||||
|
|
||||||
|
LegacyScoreAttributes attributes = new LegacyScoreAttributes();
|
||||||
|
|
||||||
foreach (var obj in playableBeatmap.HitObjects)
|
foreach (var obj in playableBeatmap.HitObjects)
|
||||||
simulateHit(obj);
|
simulateHit(obj, ref attributes);
|
||||||
|
|
||||||
|
attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore;
|
||||||
|
|
||||||
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void simulateHit(HitObject hitObject)
|
private void simulateHit(HitObject hitObject, ref LegacyScoreAttributes attributes)
|
||||||
{
|
{
|
||||||
bool increaseCombo = true;
|
bool increaseCombo = true;
|
||||||
bool addScoreComboMultiplier = false;
|
bool addScoreComboMultiplier = false;
|
||||||
@ -112,28 +111,28 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
|
|
||||||
case JuiceStream:
|
case JuiceStream:
|
||||||
foreach (var nested in hitObject.NestedHitObjects)
|
foreach (var nested in hitObject.NestedHitObjects)
|
||||||
simulateHit(nested);
|
simulateHit(nested, ref attributes);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case BananaShower:
|
case BananaShower:
|
||||||
foreach (var nested in hitObject.NestedHitObjects)
|
foreach (var nested in hitObject.NestedHitObjects)
|
||||||
simulateHit(nested);
|
simulateHit(nested, ref attributes);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addScoreComboMultiplier)
|
if (addScoreComboMultiplier)
|
||||||
{
|
{
|
||||||
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
||||||
ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier));
|
attributes.ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBonus)
|
if (isBonus)
|
||||||
{
|
{
|
||||||
legacyBonusScore += scoreIncrease;
|
legacyBonusScore += scoreIncrease;
|
||||||
modernBonusScore += Judgement.ToNumericResult(bonusResult);
|
standardisedBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
AccuracyScore += scoreIncrease;
|
attributes.AccuracyScore += scoreIncrease;
|
||||||
|
|
||||||
if (increaseCombo)
|
if (increaseCombo)
|
||||||
combo++;
|
combo++;
|
||||||
|
@ -25,6 +25,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit
|
namespace osu.Game.Rulesets.Catch.Edit
|
||||||
{
|
{
|
||||||
|
// we're also a ScrollingHitObjectComposer candidate, but can't be everything can we?
|
||||||
public partial class CatchHitObjectComposer : DistancedHitObjectComposer<CatchHitObject>
|
public partial class CatchHitObjectComposer : DistancedHitObjectComposer<CatchHitObject>
|
||||||
{
|
{
|
||||||
private const float distance_snap_radius = 50;
|
private const float distance_snap_radius = 50;
|
||||||
@ -140,7 +141,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
return base.OnPressed(e);
|
return base.OnPressed(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) =>
|
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods) =>
|
||||||
new DrawableCatchEditorRuleset(ruleset, beatmap, mods)
|
new DrawableCatchEditorRuleset(ruleset, beatmap, mods)
|
||||||
{
|
{
|
||||||
TimeRangeMultiplier = { BindTarget = timeRangeMultiplier, }
|
TimeRangeMultiplier = { BindTarget = timeRangeMultiplier, }
|
||||||
|
@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
int nodeIndex = 0;
|
int nodeIndex = 0;
|
||||||
SliderEventDescriptor? lastEvent = null;
|
SliderEventDescriptor? lastEvent = null;
|
||||||
|
|
||||||
foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
|
foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), cancellationToken))
|
||||||
{
|
{
|
||||||
// generate tiny droplets since the last point
|
// generate tiny droplets since the last point
|
||||||
if (lastEvent != null)
|
if (lastEvent != null)
|
||||||
@ -104,8 +104,8 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// this also includes LegacyLastTick and this is used for TinyDroplet generation above.
|
// this also includes LastTick and this is used for TinyDroplet generation above.
|
||||||
// this means that the final segment of TinyDroplets are increasingly mistimed where LegacyLastTickOffset is being applied.
|
// this means that the final segment of TinyDroplets are increasingly mistimed where LastTick is being applied.
|
||||||
lastEvent = e;
|
lastEvent = e;
|
||||||
|
|
||||||
switch (e.Type)
|
switch (e.Type)
|
||||||
@ -162,7 +162,5 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
public double Distance => Path.Distance;
|
public double Distance => Path.Distance;
|
||||||
|
|
||||||
public IList<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
|
public IList<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
|
||||||
|
|
||||||
public double? LegacyLastTickOffset { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,17 +2,21 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||||
{
|
{
|
||||||
public partial class LegacyBananaPiece : LegacyCatchHitObjectPiece
|
public partial class LegacyBananaPiece : LegacyCatchHitObjectPiece
|
||||||
{
|
{
|
||||||
|
private static readonly Vector2 banana_max_size = new Vector2(128);
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
Texture? texture = Skin.GetTexture("fruit-bananas");
|
Texture? texture = Skin.GetTexture("fruit-bananas")?.WithMaximumSize(banana_max_size);
|
||||||
Texture? overlayTexture = Skin.GetTexture("fruit-bananas-overlay");
|
Texture? overlayTexture = Skin.GetTexture("fruit-bananas-overlay")?.WithMaximumSize(banana_max_size);
|
||||||
|
|
||||||
SetTexture(texture, overlayTexture);
|
SetTexture(texture, overlayTexture);
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,15 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||||
{
|
{
|
||||||
public partial class LegacyDropletPiece : LegacyCatchHitObjectPiece
|
public partial class LegacyDropletPiece : LegacyCatchHitObjectPiece
|
||||||
{
|
{
|
||||||
|
private static readonly Vector2 droplet_max_size = new Vector2(82, 103);
|
||||||
|
|
||||||
public LegacyDropletPiece()
|
public LegacyDropletPiece()
|
||||||
{
|
{
|
||||||
Scale = new Vector2(0.8f);
|
Scale = new Vector2(0.8f);
|
||||||
@ -17,8 +20,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
Texture? texture = Skin.GetTexture("fruit-drop");
|
Texture? texture = Skin.GetTexture("fruit-drop")?.WithMaximumSize(droplet_max_size);
|
||||||
Texture? overlayTexture = Skin.GetTexture("fruit-drop-overlay");
|
Texture? overlayTexture = Skin.GetTexture("fruit-drop-overlay")?.WithMaximumSize(droplet_max_size);
|
||||||
|
|
||||||
SetTexture(texture, overlayTexture);
|
SetTexture(texture, overlayTexture);
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,15 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||||
{
|
{
|
||||||
internal partial class LegacyFruitPiece : LegacyCatchHitObjectPiece
|
internal partial class LegacyFruitPiece : LegacyCatchHitObjectPiece
|
||||||
{
|
{
|
||||||
|
private static readonly Vector2 fruit_max_size = new Vector2(128);
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
@ -22,21 +26,26 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
|||||||
switch (visualRepresentation)
|
switch (visualRepresentation)
|
||||||
{
|
{
|
||||||
case FruitVisualRepresentation.Pear:
|
case FruitVisualRepresentation.Pear:
|
||||||
SetTexture(Skin.GetTexture("fruit-pear"), Skin.GetTexture("fruit-pear-overlay"));
|
setTextures("pear");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FruitVisualRepresentation.Grape:
|
case FruitVisualRepresentation.Grape:
|
||||||
SetTexture(Skin.GetTexture("fruit-grapes"), Skin.GetTexture("fruit-grapes-overlay"));
|
setTextures("grapes");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FruitVisualRepresentation.Pineapple:
|
case FruitVisualRepresentation.Pineapple:
|
||||||
SetTexture(Skin.GetTexture("fruit-apple"), Skin.GetTexture("fruit-apple-overlay"));
|
setTextures("apple");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case FruitVisualRepresentation.Raspberry:
|
case FruitVisualRepresentation.Raspberry:
|
||||||
SetTexture(Skin.GetTexture("fruit-orange"), Skin.GetTexture("fruit-orange-overlay"));
|
setTextures("orange");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setTextures(string fruitName) => SetTexture(
|
||||||
|
Skin.GetTexture($"fruit-{fruitName}")?.WithMaximumSize(fruit_max_size),
|
||||||
|
Skin.GetTexture($"fruit-{fruitName}-overlay")?.WithMaximumSize(fruit_max_size)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,8 +21,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
{
|
{
|
||||||
public partial class DrawableCatchRuleset : DrawableScrollingRuleset<CatchHitObject>
|
public partial class DrawableCatchRuleset : DrawableScrollingRuleset<CatchHitObject>
|
||||||
{
|
{
|
||||||
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Constant;
|
|
||||||
|
|
||||||
protected override bool UserScrollSpeedAdjustment => false;
|
protected override bool UserScrollSpeedAdjustment => false;
|
||||||
|
|
||||||
public DrawableCatchRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
|
public DrawableCatchRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
|
||||||
@ -30,6 +28,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
{
|
{
|
||||||
Direction.Value = ScrollingDirection.Down;
|
Direction.Value = ScrollingDirection.Down;
|
||||||
TimeRange.Value = GetTimeRange(beatmap.Difficulty.ApproachRate);
|
TimeRange.Value = GetTimeRange(beatmap.Difficulty.ApproachRate);
|
||||||
|
VisualisationMethod = ScrollVisualisationMethod.Constant;
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
|||||||
PassCondition = () =>
|
PassCondition = () =>
|
||||||
{
|
{
|
||||||
var hitObject = Player.ChildrenOfType<DrawableManiaHitObject>().FirstOrDefault();
|
var hitObject = Player.ChildrenOfType<DrawableManiaHitObject>().FirstOrDefault();
|
||||||
return hitObject?.Dependencies.Get<IScrollingInfo>().Algorithm is ConstantScrollAlgorithm;
|
return hitObject?.Dependencies.Get<IScrollingInfo>().Algorithm.Value is ConstantScrollAlgorithm;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
|||||||
|
|
||||||
IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction;
|
IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction;
|
||||||
IBindable<double> IScrollingInfo.TimeRange { get; } = new Bindable<double>(5000);
|
IBindable<double> IScrollingInfo.TimeRange { get; } = new Bindable<double>(5000);
|
||||||
IScrollAlgorithm IScrollingInfo.Algorithm { get; } = new ConstantScrollAlgorithm();
|
IBindable<IScrollAlgorithm> IScrollingInfo.Algorithm { get; } = new Bindable<IScrollAlgorithm>(new ConstantScrollAlgorithm());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
175
osu.Game.Rulesets.Mania.Tests/TestSceneScoring.cs
Normal file
175
osu.Game.Rulesets.Mania.Tests/TestSceneScoring.cs
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Judgements;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Mania.Scoring;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Tests.Visual.Gameplay;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public partial class TestSceneScoring : ScoringTestScene
|
||||||
|
{
|
||||||
|
protected override IBeatmap CreateBeatmap(int maxCombo)
|
||||||
|
{
|
||||||
|
var beatmap = new ManiaBeatmap(new StageDefinition(5));
|
||||||
|
for (int i = 0; i < maxCombo; ++i)
|
||||||
|
beatmap.HitObjects.Add(new Note());
|
||||||
|
return beatmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1(MaxCombo.Value);
|
||||||
|
protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo);
|
||||||
|
protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new ManiaProcessorBasedScoringAlgorithm(beatmap, mode);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBasicScenarios()
|
||||||
|
{
|
||||||
|
AddStep("set max combo to 100", () => MaxCombo.Value = 100);
|
||||||
|
AddStep("set perfect score", () =>
|
||||||
|
{
|
||||||
|
NonPerfectLocations.Clear();
|
||||||
|
MissLocations.Clear();
|
||||||
|
});
|
||||||
|
AddStep("set score with misses", () =>
|
||||||
|
{
|
||||||
|
NonPerfectLocations.Clear();
|
||||||
|
MissLocations.Clear();
|
||||||
|
MissLocations.AddRange(new[] { 24d, 49 });
|
||||||
|
});
|
||||||
|
AddStep("set score with misses and OKs", () =>
|
||||||
|
{
|
||||||
|
NonPerfectLocations.Clear();
|
||||||
|
MissLocations.Clear();
|
||||||
|
|
||||||
|
NonPerfectLocations.AddRange(new[] { 9d, 19, 29, 39, 59, 69, 79, 89, 99 });
|
||||||
|
MissLocations.AddRange(new[] { 24d, 49 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ScoreV1 : IScoringAlgorithm
|
||||||
|
{
|
||||||
|
private int currentCombo;
|
||||||
|
private double comboAddition = 100;
|
||||||
|
private double totalScoreDouble;
|
||||||
|
private readonly double scoreMultiplier;
|
||||||
|
|
||||||
|
public ScoreV1(int maxCombo)
|
||||||
|
{
|
||||||
|
scoreMultiplier = 500000d / maxCombo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyHit() => applyHitV1(320, add => add + 2, 32);
|
||||||
|
public void ApplyNonPerfect() => applyHitV1(100, add => add - 24, 8);
|
||||||
|
public void ApplyMiss() => applyHitV1(0, _ => -56, 0);
|
||||||
|
|
||||||
|
private void applyHitV1(int scoreIncrease, Func<double, double> comboAdditionFunc, int delta)
|
||||||
|
{
|
||||||
|
comboAddition = comboAdditionFunc(comboAddition);
|
||||||
|
if (currentCombo != 0 && currentCombo % 384 == 0)
|
||||||
|
comboAddition = 100;
|
||||||
|
comboAddition = Math.Max(0, Math.Min(comboAddition, 100));
|
||||||
|
double scoreIncreaseD = Math.Sqrt(comboAddition) * delta * scoreMultiplier / 320;
|
||||||
|
|
||||||
|
TotalScore = (long)totalScoreDouble;
|
||||||
|
|
||||||
|
scoreIncreaseD += scoreIncrease * scoreMultiplier / 320;
|
||||||
|
scoreIncrease = (int)scoreIncreaseD;
|
||||||
|
|
||||||
|
TotalScore += scoreIncrease;
|
||||||
|
totalScoreDouble += scoreIncreaseD;
|
||||||
|
|
||||||
|
if (scoreIncrease > 0)
|
||||||
|
currentCombo++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long TotalScore { get; private set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ScoreV2 : IScoringAlgorithm
|
||||||
|
{
|
||||||
|
private int currentCombo;
|
||||||
|
private double comboPortion;
|
||||||
|
private double currentBaseScore;
|
||||||
|
private double maxBaseScore;
|
||||||
|
private int currentHits;
|
||||||
|
|
||||||
|
private readonly double comboPortionMax;
|
||||||
|
private readonly int maxCombo;
|
||||||
|
|
||||||
|
private const double combo_base = 4;
|
||||||
|
|
||||||
|
public ScoreV2(int maxCombo)
|
||||||
|
{
|
||||||
|
this.maxCombo = maxCombo;
|
||||||
|
|
||||||
|
for (int i = 0; i < this.maxCombo; i++)
|
||||||
|
ApplyHit();
|
||||||
|
|
||||||
|
comboPortionMax = comboPortion;
|
||||||
|
|
||||||
|
currentCombo = 0;
|
||||||
|
comboPortion = 0;
|
||||||
|
currentBaseScore = 0;
|
||||||
|
maxBaseScore = 0;
|
||||||
|
currentHits = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyHit() => applyHitV2(305, 300);
|
||||||
|
public void ApplyNonPerfect() => applyHitV2(100, 100);
|
||||||
|
|
||||||
|
private void applyHitV2(int hitValue, int baseHitValue)
|
||||||
|
{
|
||||||
|
maxBaseScore += 305;
|
||||||
|
currentBaseScore += hitValue;
|
||||||
|
comboPortion += baseHitValue * Math.Min(Math.Max(0.5, Math.Log(++currentCombo, combo_base)), Math.Log(400, combo_base));
|
||||||
|
|
||||||
|
currentHits++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyMiss()
|
||||||
|
{
|
||||||
|
currentHits++;
|
||||||
|
maxBaseScore += 305;
|
||||||
|
currentCombo = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long TotalScore
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
float accuracy = (float)(currentBaseScore / maxBaseScore);
|
||||||
|
|
||||||
|
return (int)Math.Round
|
||||||
|
(
|
||||||
|
200000 * comboPortion / comboPortionMax +
|
||||||
|
800000 * Math.Pow(accuracy, 2 + 2 * accuracy) * ((double)currentHits / maxCombo)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ManiaProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm
|
||||||
|
{
|
||||||
|
public ManiaProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode)
|
||||||
|
: base(beatmap, mode)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
|
||||||
|
|
||||||
|
protected override JudgementResult CreatePerfectJudgementResult() => new JudgementResult(new Note(), new ManiaJudgement()) { Type = HitResult.Perfect };
|
||||||
|
|
||||||
|
protected override JudgementResult CreateNonPerfectJudgementResult() => new JudgementResult(new Note(), new ManiaJudgement()) { Type = HitResult.Ok };
|
||||||
|
|
||||||
|
protected override JudgementResult CreateMissJudgementResult() => new JudgementResult(new Note(), new ManiaJudgement()) { Type = HitResult.Miss };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
@ -31,13 +31,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
|
|
||||||
public override int Version => 20220902;
|
public override int Version => 20220902;
|
||||||
|
|
||||||
private readonly IWorkingBeatmap workingBeatmap;
|
|
||||||
|
|
||||||
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
workingBeatmap = beatmap;
|
|
||||||
|
|
||||||
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
|
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
|
||||||
originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty;
|
originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty;
|
||||||
}
|
}
|
||||||
@ -60,15 +56,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject),
|
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ComputeLegacyScoringValues)
|
|
||||||
{
|
|
||||||
ManiaLegacyScoreSimulator sv1Simulator = new ManiaLegacyScoreSimulator();
|
|
||||||
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
|
||||||
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
|
||||||
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
|
||||||
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,28 +1,16 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.Mods;
|
using osu.Game.Rulesets.Scoring.Legacy;
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Rulesets.Scoring;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Difficulty
|
namespace osu.Game.Rulesets.Mania.Difficulty
|
||||||
{
|
{
|
||||||
internal class ManiaLegacyScoreSimulator : ILegacyScoreSimulator
|
internal class ManiaLegacyScoreSimulator : ILegacyScoreSimulator
|
||||||
{
|
{
|
||||||
public int AccuracyScore => 0;
|
public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap)
|
||||||
public int ComboScore { get; private set; }
|
|
||||||
public double BonusScoreRatio => 0;
|
|
||||||
|
|
||||||
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
|
||||||
{
|
{
|
||||||
double multiplier = mods.Where(m => m is not (ModHidden or ModHardRock or ModDoubleTime or ModFlashlight or ManiaModFadeIn))
|
return new LegacyScoreAttributes { ComboScore = 1000000 };
|
||||||
.Select(m => m.ScoreMultiplier)
|
|
||||||
.Aggregate(1.0, (c, n) => c * n);
|
|
||||||
|
|
||||||
ComboScore = (int)(1000000 * multiplier);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,18 +2,22 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osuTK;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Mania.UI;
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Edit
|
namespace osu.Game.Rulesets.Mania.Edit
|
||||||
{
|
{
|
||||||
public partial class DrawableManiaEditorRuleset : DrawableManiaRuleset
|
public partial class DrawableManiaEditorRuleset : DrawableManiaRuleset, ISupportConstantAlgorithmToggle
|
||||||
{
|
{
|
||||||
|
public BindableBool ShowSpeedChanges { get; set; } = new BindableBool();
|
||||||
|
|
||||||
public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;
|
public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;
|
||||||
|
|
||||||
public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods)
|
public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods)
|
||||||
@ -21,6 +25,13 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Sequential : ScrollVisualisationMethod.Constant, true);
|
||||||
|
}
|
||||||
|
|
||||||
protected override Playfield CreatePlayfield() => new ManiaEditorPlayfield(Beatmap.Stages)
|
protected override Playfield CreatePlayfield() => new ManiaEditorPlayfield(Beatmap.Stages)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
|
@ -21,7 +21,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Edit
|
namespace osu.Game.Rulesets.Mania.Edit
|
||||||
{
|
{
|
||||||
public partial class ManiaHitObjectComposer : HitObjectComposer<ManiaHitObject>
|
public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer<ManiaHitObject>
|
||||||
{
|
{
|
||||||
private DrawableManiaEditorRuleset drawableRuleset;
|
private DrawableManiaEditorRuleset drawableRuleset;
|
||||||
private ManiaBeatSnapGrid beatSnapGrid;
|
private ManiaBeatSnapGrid beatSnapGrid;
|
||||||
@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
|
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
|
||||||
Playfield.GetColumnByPosition(screenSpacePosition);
|
Playfield.GetColumnByPosition(screenSpacePosition);
|
||||||
|
|
||||||
protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||||
{
|
{
|
||||||
drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods);
|
drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods);
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ using osu.Game.Rulesets.Mania.UI;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Replays.Types;
|
using osu.Game.Rulesets.Replays.Types;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.Scoring.Legacy;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Edit.Setup;
|
using osu.Game.Screens.Edit.Setup;
|
||||||
|
@ -41,6 +41,7 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
},
|
},
|
||||||
new SettingsCheckbox
|
new SettingsCheckbox
|
||||||
{
|
{
|
||||||
|
Keywords = new[] { "color" },
|
||||||
LabelText = RulesetSettingsStrings.TimingBasedColouring,
|
LabelText = RulesetSettingsStrings.TimingBasedColouring,
|
||||||
Current = config.GetBindable<bool>(ManiaRulesetSetting.TimingBasedNoteColouring),
|
Current = config.GetBindable<bool>(ManiaRulesetSetting.TimingBasedNoteColouring),
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
public void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset)
|
public void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset)
|
||||||
{
|
{
|
||||||
var maniaRuleset = (DrawableManiaRuleset)drawableRuleset;
|
var maniaRuleset = (DrawableManiaRuleset)drawableRuleset;
|
||||||
maniaRuleset.ScrollMethod = ScrollVisualisationMethod.Constant;
|
maniaRuleset.VisualisationMethod = ScrollVisualisationMethod.Constant;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Colour;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||||
@ -25,33 +26,42 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
// Avoid flickering due to no anti-aliasing of boxes by default.
|
||||||
|
var edgeSmoothness = new Vector2(0.3f);
|
||||||
|
|
||||||
AddInternal(mainLine = new Box
|
AddInternal(mainLine = new Box
|
||||||
{
|
{
|
||||||
Name = "Bar line",
|
Name = "Bar line",
|
||||||
|
EdgeSmoothness = edgeSmoothness,
|
||||||
Anchor = Anchor.BottomCentre,
|
Anchor = Anchor.BottomCentre,
|
||||||
Origin = Anchor.BottomCentre,
|
Origin = Anchor.BottomCentre,
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
});
|
});
|
||||||
|
|
||||||
Vector2 size = new Vector2(22, 6);
|
const float major_extension = 10;
|
||||||
const float line_offset = 4;
|
|
||||||
|
|
||||||
AddInternal(leftAnchor = new Circle
|
AddInternal(leftAnchor = new Box
|
||||||
{
|
{
|
||||||
Name = "Left anchor",
|
Name = "Left anchor",
|
||||||
|
EdgeSmoothness = edgeSmoothness,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreRight,
|
Origin = Anchor.CentreRight,
|
||||||
Size = size,
|
Width = major_extension,
|
||||||
X = -line_offset,
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
Colour = ColourInfo.GradientHorizontal(Colour4.Transparent, Colour4.White),
|
||||||
});
|
});
|
||||||
|
|
||||||
AddInternal(rightAnchor = new Circle
|
AddInternal(rightAnchor = new Box
|
||||||
{
|
{
|
||||||
Name = "Right anchor",
|
Name = "Right anchor",
|
||||||
|
EdgeSmoothness = edgeSmoothness,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
Anchor = Anchor.CentreRight,
|
Anchor = Anchor.CentreRight,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Size = size,
|
Width = major_extension,
|
||||||
X = line_offset,
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
Colour = ColourInfo.GradientHorizontal(Colour4.White, Colour4.Transparent),
|
||||||
});
|
});
|
||||||
|
|
||||||
major = ((DrawableBarLine)drawableHitObject).Major.GetBoundCopy();
|
major = ((DrawableBarLine)drawableHitObject).Major.GetBoundCopy();
|
||||||
@ -66,7 +76,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default
|
|||||||
private void updateMajor(ValueChangedEvent<bool> major)
|
private void updateMajor(ValueChangedEvent<bool> major)
|
||||||
{
|
{
|
||||||
mainLine.Alpha = major.NewValue ? 0.5f : 0.2f;
|
mainLine.Alpha = major.NewValue ? 0.5f : 0.2f;
|
||||||
leftAnchor.Alpha = rightAnchor.Alpha = major.NewValue ? 1 : 0;
|
leftAnchor.Alpha = rightAnchor.Alpha = major.NewValue ? mainLine.Alpha * 0.3f : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -14,7 +13,6 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Configuration;
|
|
||||||
using osu.Game.Input.Handlers;
|
using osu.Game.Input.Handlers;
|
||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
@ -52,22 +50,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
|
|
||||||
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
|
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
|
||||||
|
|
||||||
public ScrollVisualisationMethod ScrollMethod
|
|
||||||
{
|
|
||||||
get => scrollMethod;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (IsLoaded)
|
|
||||||
throw new InvalidOperationException($"Can't alter {nameof(ScrollMethod)} after ruleset is already loaded");
|
|
||||||
|
|
||||||
scrollMethod = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ScrollVisualisationMethod scrollMethod = ScrollVisualisationMethod.Sequential;
|
|
||||||
|
|
||||||
protected override ScrollVisualisationMethod VisualisationMethod => scrollMethod;
|
|
||||||
|
|
||||||
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
|
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
|
||||||
private readonly BindableInt configScrollSpeed = new BindableInt();
|
private readonly BindableInt configScrollSpeed = new BindableInt();
|
||||||
private double smoothTimeRange;
|
private double smoothTimeRange;
|
||||||
|
@ -163,7 +163,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
slider = new Slider
|
slider = new Slider
|
||||||
{
|
{
|
||||||
Position = new Vector2(0, 50),
|
Position = new Vector2(0, 50),
|
||||||
LegacyLastTickOffset = 36, // This is necessary for undo to retain the sample control point
|
|
||||||
Path = new SliderPath(new[]
|
Path = new SliderPath(new[]
|
||||||
{
|
{
|
||||||
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
|
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
|
||||||
|
@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
[TestCase("colinear-perfect-curve")]
|
[TestCase("colinear-perfect-curve")]
|
||||||
[TestCase("slider-ticks")]
|
[TestCase("slider-ticks")]
|
||||||
[TestCase("slider-ticks-edge-case")]
|
[TestCase("slider-ticks-edge-case")]
|
||||||
|
[TestCase("slider-paths-edge-case")]
|
||||||
[TestCase("repeat-slider")]
|
[TestCase("repeat-slider")]
|
||||||
[TestCase("uneven-repeat-slider")]
|
[TestCase("uneven-repeat-slider")]
|
||||||
[TestCase("old-stacking")]
|
[TestCase("old-stacking")]
|
||||||
|
BIN
osu.Game.Rulesets.Osu.Tests/Resources/old-skin/reversearrow.png
Normal file
BIN
osu.Game.Rulesets.Osu.Tests/Resources/old-skin/reversearrow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
@ -19,7 +20,6 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps.Legacy;
|
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
@ -35,16 +35,24 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
private int depthIndex;
|
private int depthIndex;
|
||||||
|
|
||||||
private readonly BindableBool snakingIn = new BindableBool();
|
private readonly BindableBool snakingIn = new BindableBool(true);
|
||||||
private readonly BindableBool snakingOut = new BindableBool();
|
private readonly BindableBool snakingOut = new BindableBool(true);
|
||||||
|
|
||||||
[SetUpSteps]
|
private float progressToHit;
|
||||||
public void SetUpSteps()
|
|
||||||
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
AddToggleStep("toggle snaking", v =>
|
base.LoadComplete();
|
||||||
|
|
||||||
|
AddToggleStep("disable snaking", v =>
|
||||||
{
|
{
|
||||||
snakingIn.Value = v;
|
snakingIn.Value = !v;
|
||||||
snakingOut.Value = v;
|
snakingOut.Value = !v;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddSliderStep("hit at", 0f, 1f, 0f, v =>
|
||||||
|
{
|
||||||
|
progressToHit = v;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,6 +64,18 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
|
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
foreach (var slider in this.ChildrenOfType<DrawableSlider>())
|
||||||
|
{
|
||||||
|
double completionProgress = Math.Clamp((Time.Current - slider.HitObject.StartTime) / slider.HitObject.Duration, 0, 1);
|
||||||
|
if (completionProgress > progressToHit && !slider.IsHit)
|
||||||
|
slider.HeadCircle.HitArea.Hit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestVariousSliders()
|
public void TestVariousSliders()
|
||||||
{
|
{
|
||||||
@ -206,7 +226,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
StackHeight = 10
|
StackHeight = 10
|
||||||
};
|
};
|
||||||
|
|
||||||
return createDrawable(slider, 2, 2);
|
return createDrawable(slider, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Drawable testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats);
|
private Drawable testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats);
|
||||||
@ -229,6 +249,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
var slider = new Slider
|
var slider = new Slider
|
||||||
{
|
{
|
||||||
|
SliderVelocityMultiplier = speedMultiplier,
|
||||||
StartTime = Time.Current + time_offset,
|
StartTime = Time.Current + time_offset,
|
||||||
Position = new Vector2(0, -(distance / 2)),
|
Position = new Vector2(0, -(distance / 2)),
|
||||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||||
@ -240,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
StackHeight = stackHeight
|
StackHeight = stackHeight
|
||||||
};
|
};
|
||||||
|
|
||||||
return createDrawable(slider, circleSize, speedMultiplier);
|
return createDrawable(slider, circleSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Drawable testPerfect(int repeats = 0)
|
private Drawable testPerfect(int repeats = 0)
|
||||||
@ -258,7 +279,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
RepeatCount = repeats,
|
RepeatCount = repeats,
|
||||||
};
|
};
|
||||||
|
|
||||||
return createDrawable(slider, 2, 3);
|
return createDrawable(slider, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Drawable testLinear(int repeats = 0) => createLinear(repeats);
|
private Drawable testLinear(int repeats = 0) => createLinear(repeats);
|
||||||
@ -281,7 +302,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
RepeatCount = repeats,
|
RepeatCount = repeats,
|
||||||
};
|
};
|
||||||
|
|
||||||
return createDrawable(slider, 2, 3);
|
return createDrawable(slider, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Drawable testBezier(int repeats = 0) => createBezier(repeats);
|
private Drawable testBezier(int repeats = 0) => createBezier(repeats);
|
||||||
@ -303,7 +324,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
RepeatCount = repeats,
|
RepeatCount = repeats,
|
||||||
};
|
};
|
||||||
|
|
||||||
return createDrawable(slider, 2, 3);
|
return createDrawable(slider, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Drawable testLinearOverlapping(int repeats = 0) => createOverlapping(repeats);
|
private Drawable testLinearOverlapping(int repeats = 0) => createOverlapping(repeats);
|
||||||
@ -326,7 +347,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
RepeatCount = repeats,
|
RepeatCount = repeats,
|
||||||
};
|
};
|
||||||
|
|
||||||
return createDrawable(slider, 2, 3);
|
return createDrawable(slider, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Drawable testCatmull(int repeats = 0) => createCatmull(repeats);
|
private Drawable testCatmull(int repeats = 0) => createCatmull(repeats);
|
||||||
@ -352,15 +373,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
NodeSamples = repeatSamples
|
NodeSamples = repeatSamples
|
||||||
};
|
};
|
||||||
|
|
||||||
return createDrawable(slider, 3, 1);
|
return createDrawable(slider, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier)
|
private Drawable createDrawable(Slider slider, float circleSize)
|
||||||
{
|
{
|
||||||
var cpi = new LegacyControlPointInfo();
|
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty
|
||||||
cpi.Add(0, new DifficultyControlPoint { SliderVelocity = speedMultiplier });
|
|
||||||
|
|
||||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty
|
|
||||||
{
|
{
|
||||||
CircleSize = circleSize,
|
CircleSize = circleSize,
|
||||||
SliderTickRate = 3
|
SliderTickRate = 3
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -33,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
public partial class TestSceneSliderSnaking : TestSceneOsuPlayer
|
public partial class TestSceneSliderSnaking : TestSceneOsuPlayer
|
||||||
{
|
{
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private AudioManager audioManager { get; set; }
|
private AudioManager audioManager { get; set; } = null!;
|
||||||
|
|
||||||
protected override bool Autoplay => autoplay;
|
protected override bool Autoplay => autoplay;
|
||||||
private bool autoplay;
|
private bool autoplay;
|
||||||
@ -41,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private readonly BindableBool snakingIn = new BindableBool();
|
private readonly BindableBool snakingIn = new BindableBool();
|
||||||
private readonly BindableBool snakingOut = new BindableBool();
|
private readonly BindableBool snakingOut = new BindableBool();
|
||||||
|
|
||||||
private IBeatmap beatmap;
|
private IBeatmap beatmap = null!;
|
||||||
|
|
||||||
private const double duration_of_span = 3605;
|
private const double duration_of_span = 3605;
|
||||||
private const double fade_in_modifier = -1200;
|
private const double fade_in_modifier = -1200;
|
||||||
|
|
||||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
|
||||||
=> new ClockBackedTestWorkingBeatmap(this.beatmap = beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
=> new ClockBackedTestWorkingBeatmap(this.beatmap = beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -57,15 +55,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
|
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Slider slider;
|
private Slider slider = null!;
|
||||||
private DrawableSlider drawableSlider;
|
private DrawableSlider? drawableSlider;
|
||||||
|
|
||||||
[SetUp]
|
|
||||||
public void Setup() => Schedule(() =>
|
|
||||||
{
|
|
||||||
slider = null;
|
|
||||||
drawableSlider = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
protected override bool HasCustomSteps => true;
|
protected override bool HasCustomSteps => true;
|
||||||
|
|
||||||
@ -135,9 +126,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestRepeatArrowDoesNotMoveWhenHit()
|
public void TestRepeatArrowDoesNotMove([Values] bool useAutoplay)
|
||||||
{
|
{
|
||||||
AddStep("enable autoplay", () => autoplay = true);
|
AddStep($"set autoplay to {useAutoplay}", () => autoplay = useAutoplay);
|
||||||
setSnaking(true);
|
setSnaking(true);
|
||||||
CreateTest();
|
CreateTest();
|
||||||
// repeat might have a chance to update its position depending on where in the frame its hit,
|
// repeat might have a chance to update its position depending on where in the frame its hit,
|
||||||
@ -145,21 +136,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame);
|
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestRepeatArrowMovesWhenNotHit()
|
|
||||||
{
|
|
||||||
AddStep("disable autoplay", () => autoplay = false);
|
|
||||||
setSnaking(true);
|
|
||||||
CreateTest();
|
|
||||||
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void retrieveSlider(int index)
|
private void retrieveSlider(int index)
|
||||||
{
|
{
|
||||||
AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]);
|
AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]);
|
||||||
addSeekStep(() => slider.StartTime);
|
addSeekStep(() => slider.StartTime);
|
||||||
AddUntilStep("retrieve drawable slider", () =>
|
AddUntilStep("retrieve drawable slider", () =>
|
||||||
(drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
|
(drawableSlider = (DrawableSlider?)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addEnsureSnakingInSteps(Func<double> startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionIncreased);
|
private void addEnsureSnakingInSteps(Func<double> startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionIncreased);
|
||||||
@ -179,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private Func<double> timeAtRepeat(Func<double> startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex;
|
private Func<double> timeAtRepeat(Func<double> startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex;
|
||||||
private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? getSliderStart : getSliderEnd;
|
private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? getSliderStart : getSliderEnd;
|
||||||
|
|
||||||
private List<Vector2> getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
|
private List<Vector2> getSliderCurve() => ((PlaySliderBody)drawableSlider!.Body.Drawable).CurrentCurve;
|
||||||
private Vector2 getSliderStart() => getSliderCurve().First();
|
private Vector2 getSliderStart() => getSliderCurve().First();
|
||||||
private Vector2 getSliderEnd() => getSliderCurve().Last();
|
private Vector2 getSliderEnd() => getSliderCurve().Last();
|
||||||
|
|
||||||
|
@ -43,7 +43,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddUntilStep("Pitch starts low", () => getSpinningSample().Frequency.Value < 0.8);
|
AddUntilStep("Pitch starts low", () => getSpinningSample().Frequency.Value < 0.8);
|
||||||
AddUntilStep("Pitch increases", () => getSpinningSample().Frequency.Value > 0.8);
|
AddUntilStep("Pitch increases", () => getSpinningSample().Frequency.Value > 0.8);
|
||||||
|
|
||||||
PausableSkinnableSound getSpinningSample() => drawableSpinner.ChildrenOfType<PausableSkinnableSound>().FirstOrDefault(s => s.Samples.Any(i => i.LookupNames.Any(l => l.Contains("spinnerspin"))));
|
PausableSkinnableSound getSpinningSample() =>
|
||||||
|
drawableSpinner.ChildrenOfType<PausableSkinnableSound>().FirstOrDefault(s => s.Samples.Any(i => i.LookupNames.Any(l => l.Contains("spinnerspin"))));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(false)]
|
[TestCase(false)]
|
||||||
@ -64,6 +65,39 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddUntilStep("Short spinner implicitly completes", () => drawableSpinner.Progress == 1);
|
AddUntilStep("Short spinner implicitly completes", () => drawableSpinner.Progress == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase(0, 4, 6)]
|
||||||
|
[TestCase(5, 7, 10)]
|
||||||
|
[TestCase(10, 11, 8)]
|
||||||
|
public void TestSpinnerSpinRequirements(int od, int normalTicks, int bonusTicks)
|
||||||
|
{
|
||||||
|
Spinner spinner = null;
|
||||||
|
|
||||||
|
AddStep("add spinner", () => SetContents(_ =>
|
||||||
|
{
|
||||||
|
spinner = new Spinner
|
||||||
|
{
|
||||||
|
StartTime = Time.Current,
|
||||||
|
EndTime = Time.Current + 3000,
|
||||||
|
Samples = new List<HitSampleInfo>
|
||||||
|
{
|
||||||
|
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { OverallDifficulty = od });
|
||||||
|
|
||||||
|
return drawableSpinner = new TestDrawableSpinner(spinner, true)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Depth = depthIndex++,
|
||||||
|
Scale = new Vector2(0.75f)
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddAssert("number of normal ticks matches", () => spinner.SpinsRequired, () => Is.EqualTo(normalTicks));
|
||||||
|
AddAssert("number of bonus ticks matches", () => spinner.MaximumBonusSpins, () => Is.EqualTo(bonusTicks));
|
||||||
|
}
|
||||||
|
|
||||||
private Drawable testSingle(float circleSize, bool auto = false, double length = 3000)
|
private Drawable testSingle(float circleSize, bool auto = false, double length = 3000)
|
||||||
{
|
{
|
||||||
const double delay = 2000;
|
const double delay = 2000;
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||||
<PackageReference Include="Moq" Version="4.18.4" />
|
<PackageReference Include="Moq" Version="4.18.4" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
@ -44,7 +44,6 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
Position = positionData?.Position ?? Vector2.Zero,
|
Position = positionData?.Position ?? Vector2.Zero,
|
||||||
NewCombo = comboData?.NewCombo ?? false,
|
NewCombo = comboData?.NewCombo ?? false,
|
||||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||||
LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset,
|
|
||||||
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
||||||
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
||||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
|
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
|
||||||
|
@ -26,12 +26,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
public override int Version => 20220902;
|
public override int Version => 20220902;
|
||||||
|
|
||||||
private readonly IWorkingBeatmap workingBeatmap;
|
|
||||||
|
|
||||||
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
workingBeatmap = beatmap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
@ -109,15 +106,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
SpinnerCount = spinnerCount,
|
SpinnerCount = spinnerCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ComputeLegacyScoringValues)
|
|
||||||
{
|
|
||||||
OsuLegacyScoreSimulator sv1Simulator = new OsuLegacyScoreSimulator();
|
|
||||||
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
|
||||||
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
|
||||||
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
|
||||||
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,37 +2,27 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.Scoring.Legacy;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||||
{
|
{
|
||||||
internal class OsuLegacyScoreSimulator : ILegacyScoreSimulator
|
internal class OsuLegacyScoreSimulator : ILegacyScoreSimulator
|
||||||
{
|
{
|
||||||
public int AccuracyScore { get; private set; }
|
|
||||||
|
|
||||||
public int ComboScore { get; private set; }
|
|
||||||
|
|
||||||
public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore;
|
|
||||||
|
|
||||||
private int legacyBonusScore;
|
private int legacyBonusScore;
|
||||||
private int modernBonusScore;
|
private int standardisedBonusScore;
|
||||||
private int combo;
|
private int combo;
|
||||||
|
|
||||||
private double scoreMultiplier;
|
private double scoreMultiplier;
|
||||||
private IBeatmap playableBeatmap = null!;
|
|
||||||
|
|
||||||
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap)
|
||||||
{
|
{
|
||||||
this.playableBeatmap = playableBeatmap;
|
|
||||||
|
|
||||||
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
||||||
|
|
||||||
int countNormal = 0;
|
int countNormal = 0;
|
||||||
@ -73,13 +63,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
+ baseBeatmap.Difficulty.CircleSize
|
+ baseBeatmap.Difficulty.CircleSize
|
||||||
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||||
|
|
||||||
scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
scoreMultiplier = difficultyPeppyStars;
|
||||||
|
|
||||||
|
LegacyScoreAttributes attributes = new LegacyScoreAttributes();
|
||||||
|
|
||||||
foreach (var obj in playableBeatmap.HitObjects)
|
foreach (var obj in playableBeatmap.HitObjects)
|
||||||
simulateHit(obj);
|
simulateHit(obj, ref attributes);
|
||||||
|
|
||||||
|
attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore;
|
||||||
|
|
||||||
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void simulateHit(HitObject hitObject)
|
private void simulateHit(HitObject hitObject, ref LegacyScoreAttributes attributes)
|
||||||
{
|
{
|
||||||
bool increaseCombo = true;
|
bool increaseCombo = true;
|
||||||
bool addScoreComboMultiplier = false;
|
bool addScoreComboMultiplier = false;
|
||||||
@ -122,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
case Slider:
|
case Slider:
|
||||||
foreach (var nested in hitObject.NestedHitObjects)
|
foreach (var nested in hitObject.NestedHitObjects)
|
||||||
simulateHit(nested);
|
simulateHit(nested, ref attributes);
|
||||||
|
|
||||||
scoreIncrease = 300;
|
scoreIncrease = 300;
|
||||||
increaseCombo = false;
|
increaseCombo = false;
|
||||||
@ -133,22 +129,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
// The spinner object applies a lenience because gameplay mechanics differ from osu-stable.
|
// The spinner object applies a lenience because gameplay mechanics differ from osu-stable.
|
||||||
// We'll redo the calculations to match osu-stable here...
|
// We'll redo the calculations to match osu-stable here...
|
||||||
const double maximum_rotations_per_second = 477.0 / 60;
|
const double maximum_rotations_per_second = 477.0 / 60;
|
||||||
double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5);
|
|
||||||
|
// Normally, this value depends on the final overall difficulty. For simplicity, we'll only consider the worst case that maximises bonus score.
|
||||||
|
// As we're primarily concerned with computing the maximum theoretical final score,
|
||||||
|
// this will have the final effect of slightly underestimating bonus score achieved on stable when converting from score V1.
|
||||||
|
const double minimum_rotations_per_second = 3;
|
||||||
|
|
||||||
double secondsDuration = spinner.Duration / 1000;
|
double secondsDuration = spinner.Duration / 1000;
|
||||||
|
|
||||||
// The total amount of half spins possible for the entire spinner.
|
// The total amount of half spins possible for the entire spinner.
|
||||||
int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2);
|
int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2);
|
||||||
// The amount of half spins that are required to successfully complete the spinner (i.e. get a 300).
|
// The amount of half spins that are required to successfully complete the spinner (i.e. get a 300).
|
||||||
int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond);
|
int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimum_rotations_per_second);
|
||||||
// To be able to receive bonus points, the spinner must be rotated another 1.5 times.
|
// To be able to receive bonus points, the spinner must be rotated another 1.5 times.
|
||||||
int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3;
|
int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3;
|
||||||
|
|
||||||
for (int i = 0; i <= totalHalfSpinsPossible; i++)
|
for (int i = 0; i <= totalHalfSpinsPossible; i++)
|
||||||
{
|
{
|
||||||
if (i > halfSpinsRequiredBeforeBonus && (i - halfSpinsRequiredBeforeBonus) % 2 == 0)
|
if (i > halfSpinsRequiredBeforeBonus && (i - halfSpinsRequiredBeforeBonus) % 2 == 0)
|
||||||
simulateHit(new SpinnerBonusTick());
|
simulateHit(new SpinnerBonusTick(), ref attributes);
|
||||||
else if (i > 1 && i % 2 == 0)
|
else if (i > 1 && i % 2 == 0)
|
||||||
simulateHit(new SpinnerTick());
|
simulateHit(new SpinnerTick(), ref attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
scoreIncrease = 300;
|
scoreIncrease = 300;
|
||||||
@ -159,16 +160,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
if (addScoreComboMultiplier)
|
if (addScoreComboMultiplier)
|
||||||
{
|
{
|
||||||
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
||||||
ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier));
|
attributes.ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBonus)
|
if (isBonus)
|
||||||
{
|
{
|
||||||
legacyBonusScore += scoreIncrease;
|
legacyBonusScore += scoreIncrease;
|
||||||
modernBonusScore += Judgement.ToNumericResult(bonusResult);
|
standardisedBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
AccuracyScore += scoreIncrease;
|
attributes.AccuracyScore += scoreIncrease;
|
||||||
|
|
||||||
if (increaseCombo)
|
if (increaseCombo)
|
||||||
combo++;
|
combo++;
|
||||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
|
|||||||
{
|
{
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
Size = OsuHitObject.OBJECT_DIMENSIONS;
|
||||||
|
|
||||||
InternalChild = content = new Container
|
InternalChild = content = new Container
|
||||||
{
|
{
|
||||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
|
|||||||
{
|
{
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
Size = OsuHitObject.OBJECT_DIMENSIONS;
|
||||||
|
|
||||||
CornerRadius = Size.X / 2;
|
CornerRadius = Size.X / 2;
|
||||||
CornerExponent = 2;
|
CornerExponent = 2;
|
||||||
|
@ -315,7 +315,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
StartTime = HitObject.StartTime,
|
StartTime = HitObject.StartTime,
|
||||||
Position = HitObject.Position + splitControlPoints[0].Position,
|
Position = HitObject.Position + splitControlPoints[0].Position,
|
||||||
NewCombo = HitObject.NewCombo,
|
NewCombo = HitObject.NewCombo,
|
||||||
LegacyLastTickOffset = HitObject.LegacyLastTickOffset,
|
|
||||||
Samples = HitObject.Samples.Select(s => s.With()).ToList(),
|
Samples = HitObject.Samples.Select(s => s.With()).ToList(),
|
||||||
RepeatCount = HitObject.RepeatCount,
|
RepeatCount = HitObject.RepeatCount,
|
||||||
NodeSamples = HitObject.NodeSamples.Select(n => (IList<HitSampleInfo>)n.Select(s => s.With()).ToList()).ToList(),
|
NodeSamples = HitObject.NodeSamples.Select(n => (IList<HitSampleInfo>)n.Select(s => s.With()).ToList()).ToList(),
|
||||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DrawableRuleset<OsuHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
protected override DrawableRuleset<OsuHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||||
=> new DrawableOsuEditorRuleset(ruleset, beatmap, mods);
|
=> new DrawableOsuEditorRuleset(ruleset, beatmap, mods);
|
||||||
|
|
||||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
||||||
|
@ -61,10 +61,12 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||||
{
|
{
|
||||||
|
OsuHitObject firstObject = drawableRuleset.Beatmap.HitObjects.First();
|
||||||
|
|
||||||
// Multiplying by 2 results in an initial size that is too large, hence 1.90 has been chosen
|
// Multiplying by 2 results in an initial size that is too large, hence 1.90 has been chosen
|
||||||
// Also avoids the HitObject bleeding around the edges of the bubble drawable at minimum size
|
// Also avoids the HitObject bleeding around the edges of the bubble drawable at minimum size
|
||||||
bubbleSize = (float)(drawableRuleset.Beatmap.HitObjects.OfType<HitCircle>().First().Radius * 1.90f);
|
bubbleSize = (float)firstObject.Radius * 1.90f;
|
||||||
bubbleFade = drawableRuleset.Beatmap.HitObjects.OfType<HitCircle>().First().TimePreempt * 2;
|
bubbleFade = firstObject.TimePreempt * 2;
|
||||||
|
|
||||||
// We want to hide the judgements since they are obscured by the BubbleDrawable (due to layering)
|
// We want to hide the judgements since they are obscured by the BubbleDrawable (due to layering)
|
||||||
drawableRuleset.Playfield.DisplayJudgements.Value = false;
|
drawableRuleset.Playfield.DisplayJudgements.Value = false;
|
||||||
|
@ -25,9 +25,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
|
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
|
||||||
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);
|
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);
|
||||||
|
|
||||||
[SettingSource("No slider head movement", "Pins slider heads at their starting position, regardless of time.")]
|
|
||||||
public Bindable<bool> NoSliderHeadMovement { get; } = new BindableBool(true);
|
|
||||||
|
|
||||||
[SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")]
|
[SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")]
|
||||||
public Bindable<bool> ClassicNoteLock { get; } = new BindableBool(true);
|
public Bindable<bool> ClassicNoteLock { get; } = new BindableBool(true);
|
||||||
|
|
||||||
@ -71,7 +68,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
switch (obj)
|
switch (obj)
|
||||||
{
|
{
|
||||||
case DrawableSliderHead head:
|
case DrawableSliderHead head:
|
||||||
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
|
|
||||||
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
||||||
applyEarlyFading(head);
|
applyEarlyFading(head);
|
||||||
|
|
||||||
|
@ -96,14 +96,13 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
Position = original.Position;
|
Position = original.Position;
|
||||||
NewCombo = original.NewCombo;
|
NewCombo = original.NewCombo;
|
||||||
ComboOffset = original.ComboOffset;
|
ComboOffset = original.ComboOffset;
|
||||||
LegacyLastTickOffset = original.LegacyLastTickOffset;
|
|
||||||
TickDistanceMultiplier = original.TickDistanceMultiplier;
|
TickDistanceMultiplier = original.TickDistanceMultiplier;
|
||||||
SliderVelocityMultiplier = original.SliderVelocityMultiplier;
|
SliderVelocityMultiplier = original.SliderVelocityMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken);
|
var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), cancellationToken);
|
||||||
|
|
||||||
foreach (var e in sliderEvents)
|
foreach (var e in sliderEvents)
|
||||||
{
|
{
|
||||||
@ -130,7 +129,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SliderEventType.LegacyLastTick:
|
case SliderEventType.LastTick:
|
||||||
AddNested(TailCircle = new StrictTrackingSliderTailCircle(this)
|
AddNested(TailCircle = new StrictTrackingSliderTailCircle(this)
|
||||||
{
|
{
|
||||||
RepeatIndex = e.SpanIndex,
|
RepeatIndex = e.SpanIndex,
|
||||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
d.HitObjectApplied += _ =>
|
d.HitObjectApplied += _ =>
|
||||||
{
|
{
|
||||||
// slider tails are a painful edge case, as their start time is offset 36ms back (see `LegacyLastTick`).
|
// slider tails are a painful edge case, as their start time is offset 36ms back (see `LastTick`).
|
||||||
// to work around this, look up the slider tail's parenting slider's end time instead to ensure proper snap.
|
// to work around this, look up the slider tail's parenting slider's end time instead to ensure proper snap.
|
||||||
double snapTime = d is DrawableSliderTail tail
|
double snapTime = d is DrawableSliderTail tail
|
||||||
? tail.Slider.GetEndTime()
|
? tail.Slider.GetEndTime()
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -34,6 +35,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
public HitReceptor HitArea { get; private set; }
|
public HitReceptor HitArea { get; private set; }
|
||||||
public SkinnableDrawable CirclePiece { get; private set; }
|
public SkinnableDrawable CirclePiece { get; private set; }
|
||||||
|
|
||||||
|
protected override IEnumerable<Drawable> DimmablePieces => new[]
|
||||||
|
{
|
||||||
|
CirclePiece,
|
||||||
|
};
|
||||||
|
|
||||||
Drawable IHasApproachCircle.ApproachCircle => ApproachCircle;
|
Drawable IHasApproachCircle.ApproachCircle => ApproachCircle;
|
||||||
|
|
||||||
private Container scaleContainer;
|
private Container scaleContainer;
|
||||||
@ -191,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
CirclePiece.FadeInFromZero(HitObject.TimeFadeIn);
|
CirclePiece.FadeInFromZero(HitObject.TimeFadeIn);
|
||||||
|
|
||||||
ApproachCircle.FadeIn(Math.Min(HitObject.TimeFadeIn * 2, HitObject.TimePreempt));
|
ApproachCircle.FadeTo(0.9f, Math.Min(HitObject.TimeFadeIn * 2, HitObject.TimePreempt));
|
||||||
ApproachCircle.ScaleTo(1f, HitObject.TimePreempt);
|
ApproachCircle.ScaleTo(1f, HitObject.TimePreempt);
|
||||||
ApproachCircle.Expire(true);
|
ApproachCircle.Expire(true);
|
||||||
}
|
}
|
||||||
@ -244,7 +250,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
public HitReceptor()
|
public HitReceptor()
|
||||||
{
|
{
|
||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
Size = OsuHitObject.OBJECT_DIMENSIONS;
|
||||||
|
|
||||||
Anchor = Anchor.Centre;
|
Anchor = Anchor.Centre;
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -71,20 +73,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
ScaleBindable.UnbindFrom(HitObject.ScaleBindable);
|
ScaleBindable.UnbindFrom(HitObject.ScaleBindable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual IEnumerable<Drawable> DimmablePieces => Enumerable.Empty<Drawable>();
|
||||||
|
|
||||||
protected override void UpdateInitialTransforms()
|
protected override void UpdateInitialTransforms()
|
||||||
{
|
{
|
||||||
base.UpdateInitialTransforms();
|
base.UpdateInitialTransforms();
|
||||||
|
|
||||||
// Dim should only be applied at a top level, as it will be implicitly applied to nested objects.
|
foreach (var piece in DimmablePieces)
|
||||||
if (ParentHitObject == null)
|
|
||||||
{
|
{
|
||||||
// Of note, no one noticed this was missing for years, but it definitely feels like it should still exist.
|
piece.FadeColour(new Color4(195, 195, 195, 255));
|
||||||
// For now this is applied across all skins, and matches stable.
|
using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
|
||||||
// For simplicity, dim colour is applied to the DrawableHitObject itself.
|
piece.FadeColour(Color4.White, 100);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -35,6 +36,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
private ShakeContainer shakeContainer;
|
private ShakeContainer shakeContainer;
|
||||||
|
|
||||||
|
protected override IEnumerable<Drawable> DimmablePieces => new Drawable[]
|
||||||
|
{
|
||||||
|
HeadCircle,
|
||||||
|
TailCircle,
|
||||||
|
Body,
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A target container which can be used to add top level elements to the slider's display.
|
/// 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.
|
/// Intended to be used for proxy purposes only.
|
||||||
@ -228,7 +236,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);
|
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);
|
||||||
|
|
||||||
Ball.UpdateProgress(completionProgress);
|
Ball.UpdateProgress(completionProgress);
|
||||||
SliderBody?.UpdateProgress(completionProgress);
|
SliderBody?.UpdateProgress(HeadCircle.IsHit ? completionProgress : 0);
|
||||||
|
|
||||||
foreach (DrawableHitObject hitObject in NestedHitObjects)
|
foreach (DrawableHitObject hitObject in NestedHitObjects)
|
||||||
{
|
{
|
||||||
@ -288,7 +296,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
public override void PlaySamples()
|
public override void PlaySamples()
|
||||||
{
|
{
|
||||||
// rather than doing it this way, we should probably attach the sample to the tail circle.
|
// rather than doing it this way, we should probably attach the sample to the tail circle.
|
||||||
// this can only be done after we stop using LegacyLastTick.
|
// this can only be done if we stop using LastTick.
|
||||||
if (!TailCircle.SamplePlaysOnlyOnHit || TailCircle.IsHit)
|
if (!TailCircle.SamplePlaysOnlyOnHit || TailCircle.IsHit)
|
||||||
base.PlaySamples();
|
base.PlaySamples();
|
||||||
}
|
}
|
||||||
@ -317,7 +325,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
switch (state)
|
switch (state)
|
||||||
{
|
{
|
||||||
case ArmedState.Hit:
|
case ArmedState.Hit:
|
||||||
if (SliderBody?.SnakingOut.Value == true)
|
if (HeadCircle.IsHit && SliderBody?.SnakingOut.Value == true)
|
||||||
Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear.
|
Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear.
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
Size = OsuHitObject.OBJECT_DIMENSIONS;
|
||||||
|
|
||||||
Children = new[]
|
Children = new[]
|
||||||
{
|
{
|
||||||
|
@ -3,11 +3,9 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
@ -24,12 +22,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;
|
public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Makes this <see cref="DrawableSliderHead"/> track the follow circle when the start time is reached.
|
|
||||||
/// If <c>false</c>, this <see cref="DrawableSliderHead"/> will be pinned to its initial position in the slider.
|
|
||||||
/// </summary>
|
|
||||||
public bool TrackFollowCircle = true;
|
|
||||||
|
|
||||||
private readonly IBindable<int> pathVersion = new Bindable<int>();
|
private readonly IBindable<int> pathVersion = new Bindable<int>();
|
||||||
|
|
||||||
protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle;
|
protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle;
|
||||||
@ -64,23 +56,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
CheckHittable = (d, t, r) => DrawableSlider.CheckHittable?.Invoke(d, t, r) ?? ClickAction.Hit;
|
CheckHittable = (d, t, r) => DrawableSlider.CheckHittable?.Invoke(d, t, r) ?? ClickAction.Hit;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
|
||||||
{
|
|
||||||
base.Update();
|
|
||||||
|
|
||||||
Debug.Assert(Slider != null);
|
|
||||||
Debug.Assert(HitObject != null);
|
|
||||||
|
|
||||||
if (TrackFollowCircle)
|
|
||||||
{
|
|
||||||
double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1);
|
|
||||||
|
|
||||||
//todo: we probably want to reconsider this before adding scoring, but it looks and feels nice.
|
|
||||||
if (!IsHit)
|
|
||||||
Position = Slider.CurvePositionAt(completionProgress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override HitResult ResultFor(double timeOffset)
|
protected override HitResult ResultFor(double timeOffset)
|
||||||
{
|
{
|
||||||
Debug.Assert(HitObject != null);
|
Debug.Assert(HitObject != null);
|
||||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
public SkinnableDrawable CirclePiece { get; private set; }
|
public SkinnableDrawable CirclePiece { get; private set; }
|
||||||
|
|
||||||
public ReverseArrowPiece Arrow { get; private set; }
|
public SkinnableDrawable Arrow { get; private set; }
|
||||||
|
|
||||||
private Drawable scaleContainer;
|
private Drawable scaleContainer;
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
Size = OsuHitObject.OBJECT_DIMENSIONS;
|
||||||
|
|
||||||
AddInternal(scaleContainer = new Container
|
AddInternal(scaleContainer = new Container
|
||||||
{
|
{
|
||||||
@ -65,7 +65,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
},
|
},
|
||||||
Arrow = new ReverseArrowPiece(),
|
Arrow = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.ReverseArrow), _ => new DefaultReverseArrow())
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
Size = OsuHitObject.OBJECT_DIMENSIONS;
|
||||||
|
|
||||||
AddRangeInternal(new Drawable[]
|
AddRangeInternal(new Drawable[]
|
||||||
{
|
{
|
||||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
Size = OsuHitObject.OBJECT_DIMENSIONS;
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
AddInternal(scaleContainer = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderScorePoint), _ => new CircularContainer
|
AddInternal(scaleContainer = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderScorePoint), _ => new CircularContainer
|
||||||
|
@ -21,6 +21,11 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const float OBJECT_RADIUS = 64;
|
public const float OBJECT_RADIUS = 64;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The width and height any element participating in display of a hitcircle (or similarly sized object) should be.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly Vector2 OBJECT_DIMENSIONS = new Vector2(OBJECT_RADIUS * 2);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Scoring distance with a speed-adjusted beat length of 1 second (ie. the speed slider balls move through their track).
|
/// Scoring distance with a speed-adjusted beat length of 1 second (ie. the speed slider balls move through their track).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -71,8 +71,6 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public double? LegacyLastTickOffset { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The position of the cursor at the point of completion of this <see cref="Slider"/> if it was hit
|
/// The position of the cursor at the point of completion of this <see cref="Slider"/> if it was hit
|
||||||
/// with as few movements as possible. This is set and used by difficulty calculation.
|
/// with as few movements as possible. This is set and used by difficulty calculation.
|
||||||
@ -179,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
base.CreateNestedHitObjects(cancellationToken);
|
base.CreateNestedHitObjects(cancellationToken);
|
||||||
|
|
||||||
var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken);
|
var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), cancellationToken);
|
||||||
|
|
||||||
foreach (var e in sliderEvents)
|
foreach (var e in sliderEvents)
|
||||||
{
|
{
|
||||||
@ -206,10 +204,11 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SliderEventType.LegacyLastTick:
|
case SliderEventType.LastTick:
|
||||||
// we need to use the LegacyLastTick here for compatibility reasons (difficulty).
|
// Of note, we are directly mapping LastTick (instead of `SliderEventType.Tail`) to SliderTailCircle.
|
||||||
// it is *okay* to use this because the TailCircle is not used for any meaningful purpose in gameplay.
|
// It is required as difficulty calculation and gameplay relies on reading this value.
|
||||||
// if this is to change, we should revisit this.
|
// (although it is displayed in classic skins, which may be a concern).
|
||||||
|
// If this is to change, we should revisit this.
|
||||||
AddNested(TailCircle = new SliderTailCircle(this)
|
AddNested(TailCircle = new SliderTailCircle(this)
|
||||||
{
|
{
|
||||||
RepeatIndex = e.SpanIndex,
|
RepeatIndex = e.SpanIndex,
|
||||||
@ -264,7 +263,9 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
if (HeadCircle != null)
|
if (HeadCircle != null)
|
||||||
HeadCircle.Samples = this.GetNodeSamples(0);
|
HeadCircle.Samples = this.GetNodeSamples(0);
|
||||||
|
|
||||||
// The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
|
// The samples should be attached to the slider tail, however this can only be done if LastTick is removed otherwise they would play earlier than they're intended to.
|
||||||
|
// (see mapping logic in `CreateNestedHitObjects` above)
|
||||||
|
//
|
||||||
// For now, the samples are played by the slider itself at the correct end time.
|
// For now, the samples are played by the slider itself at the correct end time.
|
||||||
TailSamples = this.GetNodeSamples(repeatCount + 1);
|
TailSamples = this.GetNodeSamples(repeatCount + 1);
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Note that this should not be used for timing correctness.
|
/// Note that this should not be used for timing correctness.
|
||||||
/// See <see cref="SliderEventType.LegacyLastTick"/> usage in <see cref="Slider"/> for more information.
|
/// See <see cref="SliderEventType.LastTick"/> usage in <see cref="Slider"/> for more information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SliderTailCircle : SliderEndCircle
|
public class SliderTailCircle : SliderEndCircle
|
||||||
{
|
{
|
||||||
|
@ -18,6 +18,16 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
public class Spinner : OsuHitObject, IHasDuration
|
public class Spinner : OsuHitObject, IHasDuration
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The RPM required to clear the spinner at ODs [ 0, 5, 10 ].
|
||||||
|
/// </summary>
|
||||||
|
private static readonly (int min, int mid, int max) clear_rpm_range = (90, 150, 225);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The RPM required to complete the spinner and receive full score at ODs [ 0, 5, 10 ].
|
||||||
|
/// </summary>
|
||||||
|
private static readonly (int min, int mid, int max) complete_rpm_range = (250, 380, 430);
|
||||||
|
|
||||||
public double EndTime
|
public double EndTime
|
||||||
{
|
{
|
||||||
get => StartTime + Duration;
|
get => StartTime + Duration;
|
||||||
@ -52,13 +62,19 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||||
|
|
||||||
const double maximum_rotations_per_second = 477f / 60f;
|
// The average RPS required over the length of the spinner to clear the spinner.
|
||||||
|
double minRps = IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, clear_rpm_range) / 60;
|
||||||
|
|
||||||
|
// The RPS required over the length of the spinner to receive full score (all normal + bonus ticks).
|
||||||
|
double maxRps = IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, complete_rpm_range) / 60;
|
||||||
|
|
||||||
double secondsDuration = Duration / 1000;
|
double secondsDuration = Duration / 1000;
|
||||||
double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 1.5, 2.5, 3.75);
|
|
||||||
|
|
||||||
SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond);
|
// Allow a 0.1ms floating point precision error in the calculation of the duration.
|
||||||
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration) - bonus_spins_gap;
|
const double duration_error = 0.0001;
|
||||||
|
|
||||||
|
SpinsRequired = (int)(minRps * secondsDuration + duration_error);
|
||||||
|
MaximumBonusSpins = Math.Max(0, (int)(maxRps * secondsDuration + duration_error) - SpinsRequired - bonus_spins_gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
|
@ -33,6 +33,7 @@ using osu.Game.Rulesets.Osu.Statistics;
|
|||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Replays.Types;
|
using osu.Game.Rulesets.Replays.Types;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.Scoring.Legacy;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Edit.Setup;
|
using osu.Game.Screens.Edit.Setup;
|
||||||
|
@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"Mappings": [
|
||||||
|
{
|
||||||
|
"StartTime": 46060.0,
|
||||||
|
"Objects": [
|
||||||
|
{
|
||||||
|
"StartTime": 46060.0,
|
||||||
|
"EndTime": 46060.0,
|
||||||
|
"X": 160.0,
|
||||||
|
"Y": 208.0,
|
||||||
|
"StackOffset": {
|
||||||
|
"X": 0.0,
|
||||||
|
"Y": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"StartTime": 46398.0,
|
||||||
|
"EndTime": 46398.0,
|
||||||
|
"X": 160.980164,
|
||||||
|
"Y": 317.779083,
|
||||||
|
"StackOffset": {
|
||||||
|
"X": 0.0,
|
||||||
|
"Y": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"StartTime": 46737.0,
|
||||||
|
"EndTime": 46737.0,
|
||||||
|
"X": 268.887268,
|
||||||
|
"Y": 320.0,
|
||||||
|
"StackOffset": {
|
||||||
|
"X": 0.0,
|
||||||
|
"Y": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"StartTime": 47040.0,
|
||||||
|
"EndTime": 47040.0,
|
||||||
|
"X": 378.995544,
|
||||||
|
"Y": 320.0,
|
||||||
|
"StackOffset": {
|
||||||
|
"X": 0.0,
|
||||||
|
"Y": 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"StartTime": 123348.0,
|
||||||
|
"Objects": [
|
||||||
|
{
|
||||||
|
"StartTime": 123348.0,
|
||||||
|
"EndTime": 123348.0,
|
||||||
|
"X": 352.0,
|
||||||
|
"Y": 160.0,
|
||||||
|
"StackOffset": {
|
||||||
|
"X": 0.0,
|
||||||
|
"Y": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"StartTime": 123686.0,
|
||||||
|
"EndTime": 123686.0,
|
||||||
|
"X": 351.019836,
|
||||||
|
"Y": 50.2209129,
|
||||||
|
"StackOffset": {
|
||||||
|
"X": 0.0,
|
||||||
|
"Y": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"StartTime": 124025.0,
|
||||||
|
"EndTime": 124025.0,
|
||||||
|
"X": 243.112747,
|
||||||
|
"Y": 48.0,
|
||||||
|
"StackOffset": {
|
||||||
|
"X": 0.0,
|
||||||
|
"Y": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"StartTime": 124328.0,
|
||||||
|
"EndTime": 124328.0,
|
||||||
|
"X": 133.004471,
|
||||||
|
"Y": 48.0,
|
||||||
|
"StackOffset": {
|
||||||
|
"X": 0.0,
|
||||||
|
"Y": 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
osu file format v6
|
||||||
|
|
||||||
|
[General]
|
||||||
|
StackLeniency: 0.7
|
||||||
|
|
||||||
|
[Difficulty]
|
||||||
|
HPDrainRate:1
|
||||||
|
CircleSize:3
|
||||||
|
OverallDifficulty:1
|
||||||
|
SliderMultiplier:1.1
|
||||||
|
SliderTickRate:1
|
||||||
|
|
||||||
|
[TimingPoints]
|
||||||
|
-41,338.983050847458,4,1,0,70,1,0
|
||||||
|
93648,-100,4,1,0,70,0,0
|
||||||
|
|
||||||
|
[HitObjects]
|
||||||
|
160,208,46060,6,0,B|161:320|161:320|271:320|271:320,1,330,8|0
|
||||||
|
352,160,123348,6,0,B|351:48|351:48|241:48|241:48,1,330,8|0
|
@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
|
|
||||||
private Bindable<bool> configHitLighting = null!;
|
private Bindable<bool> configHitLighting = null!;
|
||||||
|
|
||||||
private static readonly Vector2 circle_size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
private static readonly Vector2 circle_size = OsuHitObject.OBJECT_DIMENSIONS;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private DrawableHitObject drawableObject { get; set; } = null!;
|
private DrawableHitObject drawableObject { get; set; } = null!;
|
||||||
|
@ -4,10 +4,12 @@
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -17,19 +19,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
{
|
{
|
||||||
public partial class ArgonReverseArrow : CompositeDrawable
|
public partial class ArgonReverseArrow : CompositeDrawable
|
||||||
{
|
{
|
||||||
|
[Resolved]
|
||||||
|
private DrawableHitObject drawableObject { get; set; } = null!;
|
||||||
|
|
||||||
private Bindable<Color4> accentColour = null!;
|
private Bindable<Color4> accentColour = null!;
|
||||||
|
|
||||||
private SpriteIcon icon = null!;
|
private SpriteIcon icon = null!;
|
||||||
|
private Container main = null!;
|
||||||
|
private Sprite side = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(DrawableHitObject hitObject)
|
private void load(TextureStore textures)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre;
|
Anchor = Anchor.Centre;
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
Size = OsuHitObject.OBJECT_DIMENSIONS;
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
main = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new Circle
|
new Circle
|
||||||
{
|
{
|
||||||
@ -45,10 +59,52 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
side = new Sprite
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Texture = textures.Get("Gameplay/osu/repeat-edge-piece"),
|
||||||
|
Size = new Vector2(ArgonMainCirclePiece.OUTER_GRADIENT_SIZE),
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
accentColour = hitObject.AccentColour.GetBoundCopy();
|
accentColour = drawableObject.AccentColour.GetBoundCopy();
|
||||||
accentColour.BindValueChanged(accent => icon.Colour = accent.NewValue.Darken(4), true);
|
accentColour.BindValueChanged(accent => icon.Colour = accent.NewValue.Darken(4), true);
|
||||||
|
|
||||||
|
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
|
||||||
|
{
|
||||||
|
const float move_distance = -12;
|
||||||
|
const double move_out_duration = 35;
|
||||||
|
const double move_in_duration = 250;
|
||||||
|
const double total = 300;
|
||||||
|
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case ArmedState.Idle:
|
||||||
|
main.ScaleTo(1.3f, move_out_duration, Easing.Out)
|
||||||
|
.Then()
|
||||||
|
.ScaleTo(1f, move_in_duration, Easing.Out)
|
||||||
|
.Loop(total - (move_in_duration + move_out_duration));
|
||||||
|
side
|
||||||
|
.MoveToX(move_distance, move_out_duration, Easing.Out)
|
||||||
|
.Then()
|
||||||
|
.MoveToX(0, move_in_duration, Easing.Out)
|
||||||
|
.Loop(total - (move_in_duration + move_out_duration));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
if (drawableObject.IsNotNull())
|
||||||
|
drawableObject.ApplyCustomUpdateState -= updateStateTransforms;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ using osu.Framework.Graphics.Sprites;
|
|||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||||
{
|
{
|
||||||
@ -22,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
|
|
||||||
public CirclePiece()
|
public CirclePiece()
|
||||||
{
|
{
|
||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
Size = OsuHitObject.OBJECT_DIMENSIONS;
|
||||||
Masking = true;
|
Masking = true;
|
||||||
|
|
||||||
CornerRadius = Size.X / 2;
|
CornerRadius = Size.X / 2;
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
// 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.Extensions.ObjectExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||||
|
{
|
||||||
|
public partial class DefaultReverseArrow : CompositeDrawable
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private DrawableHitObject drawableObject { get; set; } = null!;
|
||||||
|
|
||||||
|
public DefaultReverseArrow()
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre;
|
||||||
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
|
Size = OsuHitObject.OBJECT_DIMENSIONS;
|
||||||
|
|
||||||
|
InternalChild = new SpriteIcon
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
Icon = FontAwesome.Solid.ChevronRight,
|
||||||
|
Size = new Vector2(0.35f),
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
|
||||||
|
{
|
||||||
|
const double move_out_duration = 35;
|
||||||
|
const double move_in_duration = 250;
|
||||||
|
const double total = 300;
|
||||||
|
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case ArmedState.Idle:
|
||||||
|
InternalChild.ScaleTo(1.3f, move_out_duration, Easing.Out)
|
||||||
|
.Then()
|
||||||
|
.ScaleTo(1f, move_in_duration, Easing.Out)
|
||||||
|
.Loop(total - (move_in_duration + move_out_duration));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
if (drawableObject.IsNotNull())
|
||||||
|
drawableObject.ApplyCustomUpdateState -= updateStateTransforms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,6 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||||
{
|
{
|
||||||
@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
|
|
||||||
public ExplodePiece()
|
public ExplodePiece()
|
||||||
{
|
{
|
||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
Size = OsuHitObject.OBJECT_DIMENSIONS;
|
||||||
|
|
||||||
Anchor = Anchor.Centre;
|
Anchor = Anchor.Centre;
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
@ -5,7 +5,6 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||||
{
|
{
|
||||||
@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
{
|
{
|
||||||
public FlashPiece()
|
public FlashPiece()
|
||||||
{
|
{
|
||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
Size = OsuHitObject.OBJECT_DIMENSIONS;
|
||||||
|
|
||||||
Anchor = Anchor.Centre;
|
Anchor = Anchor.Centre;
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
using osuTK;
|
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||||
@ -25,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
|
|
||||||
public MainCirclePiece()
|
public MainCirclePiece()
|
||||||
{
|
{
|
||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
Size = OsuHitObject.OBJECT_DIMENSIONS;
|
||||||
|
|
||||||
Anchor = Anchor.Centre;
|
Anchor = Anchor.Centre;
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
@ -46,22 +46,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
|
|
||||||
BorderSize = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1;
|
BorderSize = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1;
|
||||||
BorderColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBorder)?.Value ?? Color4.White;
|
BorderColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBorder)?.Value ?? Color4.White;
|
||||||
|
|
||||||
drawableObject.HitObjectApplied += onHitObjectApplied;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onHitObjectApplied(DrawableHitObject obj)
|
|
||||||
{
|
|
||||||
var drawableSlider = (DrawableSlider)obj;
|
|
||||||
if (drawableSlider.HitObject == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// When not tracking the follow circle, unbind from the config and forcefully disable snaking out - it looks better that way.
|
|
||||||
if (!drawableSlider.HeadCircle.TrackFollowCircle)
|
|
||||||
{
|
|
||||||
SnakingOut.UnbindFrom(configSnakingOut);
|
|
||||||
SnakingOut.Value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour) =>
|
protected virtual Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour) =>
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Audio.Track;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Sprites;
|
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
|
||||||
using osu.Game.Graphics.Containers;
|
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
using osu.Game.Skinning;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|
||||||
{
|
|
||||||
public partial class ReverseArrowPiece : BeatSyncedContainer
|
|
||||||
{
|
|
||||||
[Resolved]
|
|
||||||
private DrawableHitObject drawableRepeat { get; set; } = null!;
|
|
||||||
|
|
||||||
public ReverseArrowPiece()
|
|
||||||
{
|
|
||||||
Divisor = 2;
|
|
||||||
MinimumBeatLength = 200;
|
|
||||||
|
|
||||||
Anchor = Anchor.Centre;
|
|
||||||
Origin = Anchor.Centre;
|
|
||||||
|
|
||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
|
||||||
|
|
||||||
Child = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.ReverseArrow), _ => new SpriteIcon
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Blending = BlendingParameters.Additive,
|
|
||||||
Icon = FontAwesome.Solid.ChevronRight,
|
|
||||||
Size = new Vector2(0.35f)
|
|
||||||
})
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
|
|
||||||
{
|
|
||||||
if (!drawableRepeat.IsHit)
|
|
||||||
Child.ScaleTo(1.3f).ScaleTo(1f, timingPoint.BeatLength, Easing.Out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,7 +5,6 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osuTK;
|
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||||
@ -14,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
{
|
{
|
||||||
public RingPiece(float thickness = 9)
|
public RingPiece(float thickness = 9)
|
||||||
{
|
{
|
||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
Size = OsuHitObject.OBJECT_DIMENSIONS;
|
||||||
|
|
||||||
Anchor = Anchor.Centre;
|
Anchor = Anchor.Centre;
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
@ -5,12 +5,14 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||||
{
|
{
|
||||||
|
// todo: this should probably not be a SkinnableSprite, as this is always created for legacy skins and is recreated on skin change.
|
||||||
public partial class LegacyApproachCircle : SkinnableSprite
|
public partial class LegacyApproachCircle : SkinnableSprite
|
||||||
{
|
{
|
||||||
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
|
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
|
||||||
@ -19,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
private DrawableHitObject drawableObject { get; set; } = null!;
|
private DrawableHitObject drawableObject { get; set; } = null!;
|
||||||
|
|
||||||
public LegacyApproachCircle()
|
public LegacyApproachCircle()
|
||||||
: base("Gameplay/osu/approachcircle")
|
: base("Gameplay/osu/approachcircle", OsuHitObject.OBJECT_DIMENSIONS)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,6 @@ using osu.Game.Rulesets.Objects.Drawables;
|
|||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||||
@ -51,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
this.priorityLookupPrefix = priorityLookupPrefix;
|
this.priorityLookupPrefix = priorityLookupPrefix;
|
||||||
this.hasNumber = hasNumber;
|
this.hasNumber = hasNumber;
|
||||||
|
|
||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
Size = OsuHitObject.OBJECT_DIMENSIONS;
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -68,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
|
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
|
||||||
InternalChildren = new[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
|
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS) })
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -77,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d))
|
Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d, maxSize: OsuHitObject.OBJECT_DIMENSIONS))
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
|
@ -4,9 +4,11 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -15,8 +17,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
public partial class LegacyReverseArrow : CompositeDrawable
|
public partial class LegacyReverseArrow : CompositeDrawable
|
||||||
{
|
{
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved]
|
||||||
private DrawableHitObject? drawableHitObject { get; set; }
|
private DrawableHitObject drawableObject { get; set; } = null!;
|
||||||
|
|
||||||
private Drawable proxy = null!;
|
private Drawable proxy = null!;
|
||||||
|
|
||||||
@ -26,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
private Drawable arrow = null!;
|
private Drawable arrow = null!;
|
||||||
|
|
||||||
|
private bool shouldRotate;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(ISkinSource skinSource)
|
private void load(ISkinSource skinSource)
|
||||||
{
|
{
|
||||||
@ -35,8 +39,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null);
|
var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null);
|
||||||
|
|
||||||
InternalChild = arrow = (skin?.GetAnimation(lookupName, true, true) ?? Empty());
|
InternalChild = arrow = (skin?.GetAnimation(lookupName, true, true, maxSize: OsuHitObject.OBJECT_DIMENSIONS) ?? Empty()).With(d =>
|
||||||
|
{
|
||||||
|
d.Anchor = Anchor.Centre;
|
||||||
|
d.Origin = Anchor.Centre;
|
||||||
|
});
|
||||||
|
|
||||||
textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin;
|
textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin;
|
||||||
|
|
||||||
|
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
|
||||||
|
|
||||||
|
shouldRotate = skinSource.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value <= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
@ -45,18 +58,15 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
proxy = CreateProxy();
|
proxy = CreateProxy();
|
||||||
|
|
||||||
if (drawableHitObject != null)
|
drawableObject.HitObjectApplied += onHitObjectApplied;
|
||||||
{
|
onHitObjectApplied(drawableObject);
|
||||||
drawableHitObject.HitObjectApplied += onHitObjectApplied;
|
|
||||||
onHitObjectApplied(drawableHitObject);
|
|
||||||
|
|
||||||
accentColour = drawableHitObject.AccentColour.GetBoundCopy();
|
accentColour = drawableObject.AccentColour.GetBoundCopy();
|
||||||
accentColour.BindValueChanged(c =>
|
accentColour.BindValueChanged(c =>
|
||||||
{
|
{
|
||||||
arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > (600 / 255f) ? Color4.Black : Color4.White;
|
arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > (600 / 255f) ? Color4.Black : Color4.White;
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void onHitObjectApplied(DrawableHitObject drawableObject)
|
private void onHitObjectApplied(DrawableHitObject drawableObject)
|
||||||
{
|
{
|
||||||
@ -67,11 +77,43 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
.OverlayElementContainer.Add(proxy);
|
.OverlayElementContainer.Add(proxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
|
||||||
|
{
|
||||||
|
const double duration = 300;
|
||||||
|
const float rotation = 5.625f;
|
||||||
|
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case ArmedState.Idle:
|
||||||
|
if (shouldRotate)
|
||||||
|
{
|
||||||
|
InternalChild.ScaleTo(1.3f)
|
||||||
|
.RotateTo(rotation)
|
||||||
|
.Then()
|
||||||
|
.ScaleTo(1f, duration)
|
||||||
|
.RotateTo(-rotation, duration)
|
||||||
|
.Loop();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
InternalChild.ScaleTo(1.3f).Then()
|
||||||
|
.ScaleTo(1f, duration, Easing.Out)
|
||||||
|
.Loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
if (drawableHitObject != null)
|
|
||||||
drawableHitObject.HitObjectApplied -= onHitObjectApplied;
|
if (drawableObject.IsNotNull())
|
||||||
|
{
|
||||||
|
drawableObject.HitObjectApplied -= onHitObjectApplied;
|
||||||
|
drawableObject.ApplyCustomUpdateState -= updateStateTransforms;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Texture = skin.GetTexture("sliderb-nd"),
|
Texture = skin.GetTexture("sliderb-nd")?.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS),
|
||||||
Colour = new Color4(5, 5, 5, 255),
|
Colour = new Color4(5, 5, 5, 255),
|
||||||
},
|
},
|
||||||
LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d =>
|
LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d =>
|
||||||
@ -58,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Texture = skin.GetTexture("sliderb-spec"),
|
Texture = skin.GetTexture("sliderb-spec")?.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS),
|
||||||
Blending = BlendingParameters.Additive,
|
Blending = BlendingParameters.Additive,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
/// Their hittable area is 128px, but the actual circle portion is 118px.
|
/// Their hittable area is 128px, but the actual circle portion is 118px.
|
||||||
/// We must account for some gameplay elements such as slider bodies, where this padding is not present.
|
/// We must account for some gameplay elements such as slider bodies, where this padding is not present.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const float LEGACY_CIRCLE_RADIUS = 64 - 5;
|
public const float LEGACY_CIRCLE_RADIUS = OsuHitObject.OBJECT_RADIUS - 5;
|
||||||
|
|
||||||
public OsuLegacySkinTransformer(ISkin skin)
|
public OsuLegacySkinTransformer(ISkin skin)
|
||||||
: base(skin)
|
: base(skin)
|
||||||
@ -41,14 +42,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
return this.GetAnimation("sliderscorepoint", false, false);
|
return this.GetAnimation("sliderscorepoint", false, false);
|
||||||
|
|
||||||
case OsuSkinComponents.SliderFollowCircle:
|
case OsuSkinComponents.SliderFollowCircle:
|
||||||
var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true);
|
var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true, maxSize: new Vector2(308f));
|
||||||
if (followCircleContent != null)
|
if (followCircleContent != null)
|
||||||
return new LegacyFollowCircle(followCircleContent);
|
return new LegacyFollowCircle(followCircleContent);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
case OsuSkinComponents.SliderBall:
|
case OsuSkinComponents.SliderBall:
|
||||||
var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "");
|
var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "", maxSize: OsuHitObject.OBJECT_DIMENSIONS);
|
||||||
|
|
||||||
// todo: slider ball has a custom frame delay based on velocity
|
// todo: slider ball has a custom frame delay based on velocity
|
||||||
// Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME);
|
// Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME);
|
||||||
@ -138,7 +139,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
if (!this.HasFont(LegacyFont.HitCircle))
|
if (!this.HasFont(LegacyFont.HitCircle))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return new LegacySpriteText(LegacyFont.HitCircle)
|
return new LegacySpriteText(LegacyFont.HitCircle, OsuHitObject.OBJECT_DIMENSIONS)
|
||||||
{
|
{
|
||||||
// stable applies a blanket 0.8x scale to hitcircle fonts
|
// stable applies a blanket 0.8x scale to hitcircle fonts
|
||||||
Scale = new Vector2(0.8f),
|
Scale = new Vector2(0.8f),
|
||||||
|
@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
{
|
{
|
||||||
new RingPiece(3)
|
new RingPiece(3)
|
||||||
{
|
{
|
||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2),
|
Size = OsuHitObject.OBJECT_DIMENSIONS,
|
||||||
Alpha = 0.1f,
|
Alpha = 0.1f,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
@ -25,12 +25,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
|
|
||||||
public override int Version => 20220902;
|
public override int Version => 20220902;
|
||||||
|
|
||||||
private readonly IWorkingBeatmap workingBeatmap;
|
|
||||||
|
|
||||||
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
workingBeatmap = beatmap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||||
@ -99,15 +96,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ComputeLegacyScoringValues)
|
|
||||||
{
|
|
||||||
TaikoLegacyScoreSimulator sv1Simulator = new TaikoLegacyScoreSimulator();
|
|
||||||
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
|
||||||
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
|
||||||
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
|
||||||
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,39 +2,29 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.Scoring.Legacy;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||||
{
|
{
|
||||||
internal class TaikoLegacyScoreSimulator : ILegacyScoreSimulator
|
internal class TaikoLegacyScoreSimulator : ILegacyScoreSimulator
|
||||||
{
|
{
|
||||||
public int AccuracyScore { get; private set; }
|
|
||||||
|
|
||||||
public int ComboScore { get; private set; }
|
|
||||||
|
|
||||||
public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore;
|
|
||||||
|
|
||||||
private int legacyBonusScore;
|
private int legacyBonusScore;
|
||||||
private int modernBonusScore;
|
private int standardisedBonusScore;
|
||||||
private int combo;
|
private int combo;
|
||||||
|
|
||||||
private double modMultiplier;
|
|
||||||
private int difficultyPeppyStars;
|
private int difficultyPeppyStars;
|
||||||
private IBeatmap playableBeatmap = null!;
|
private IBeatmap playableBeatmap = null!;
|
||||||
private IReadOnlyList<Mod> mods = null!;
|
|
||||||
|
|
||||||
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap)
|
||||||
{
|
{
|
||||||
this.playableBeatmap = playableBeatmap;
|
this.playableBeatmap = playableBeatmap;
|
||||||
this.mods = mods;
|
|
||||||
|
|
||||||
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
||||||
|
|
||||||
@ -76,13 +66,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
+ baseBeatmap.Difficulty.CircleSize
|
+ baseBeatmap.Difficulty.CircleSize
|
||||||
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||||
|
|
||||||
modMultiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
LegacyScoreAttributes attributes = new LegacyScoreAttributes();
|
||||||
|
|
||||||
foreach (var obj in playableBeatmap.HitObjects)
|
foreach (var obj in playableBeatmap.HitObjects)
|
||||||
simulateHit(obj);
|
simulateHit(obj, ref attributes);
|
||||||
|
|
||||||
|
attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore;
|
||||||
|
|
||||||
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void simulateHit(HitObject hitObject)
|
private void simulateHit(HitObject hitObject, ref LegacyScoreAttributes attributes)
|
||||||
{
|
{
|
||||||
bool increaseCombo = true;
|
bool increaseCombo = true;
|
||||||
bool addScoreComboMultiplier = false;
|
bool addScoreComboMultiplier = false;
|
||||||
@ -109,21 +103,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
case Swell swell:
|
case Swell swell:
|
||||||
// The taiko swell generally does not match the osu-stable implementation in any way.
|
// The taiko swell generally does not match the osu-stable implementation in any way.
|
||||||
// We'll redo the calculations to match osu-stable here...
|
// We'll redo the calculations to match osu-stable here...
|
||||||
double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5);
|
|
||||||
double secondsDuration = swell.Duration / 1000;
|
// Normally, this value depends on the final overall difficulty. For simplicity, we'll only consider the worst case that maximises rotations.
|
||||||
|
const double minimum_rotations_per_second = 7.5;
|
||||||
|
|
||||||
// The amount of half spins that are required to successfully complete the spinner (i.e. get a 300).
|
// The amount of half spins that are required to successfully complete the spinner (i.e. get a 300).
|
||||||
int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond);
|
int halfSpinsRequiredForCompletion = (int)(swell.Duration / 1000 * minimum_rotations_per_second);
|
||||||
|
|
||||||
halfSpinsRequiredForCompletion = (int)Math.Max(1, halfSpinsRequiredForCompletion * 1.65f);
|
halfSpinsRequiredForCompletion = (int)Math.Max(1, halfSpinsRequiredForCompletion * 1.65f);
|
||||||
|
|
||||||
if (mods.Any(m => m is ModDoubleTime))
|
//
|
||||||
halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 0.75f));
|
// Normally, this multiplier depends on the active mods (DT = 0.75, HT = 1.5). For simplicity, we'll only consider the worst case that maximises rotations.
|
||||||
if (mods.Any(m => m is ModHalfTime))
|
// This way, scores remain beatable at the cost of the conversion being slightly inaccurate.
|
||||||
|
// - A perfect DT/NM score will have less than 1M total score (excluding bonus).
|
||||||
|
// - A perfect HT score will have 1M total score (excluding bonus).
|
||||||
|
//
|
||||||
halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 1.5f));
|
halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 1.5f));
|
||||||
|
|
||||||
for (int i = 0; i <= halfSpinsRequiredForCompletion; i++)
|
for (int i = 0; i <= halfSpinsRequiredForCompletion; i++)
|
||||||
simulateHit(new SwellTick());
|
simulateHit(new SwellTick(), ref attributes);
|
||||||
|
|
||||||
scoreIncrease = 300;
|
scoreIncrease = 300;
|
||||||
addScoreComboMultiplier = true;
|
addScoreComboMultiplier = true;
|
||||||
@ -139,7 +136,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
|
|
||||||
case DrumRoll:
|
case DrumRoll:
|
||||||
foreach (var nested in hitObject.NestedHitObjects)
|
foreach (var nested in hitObject.NestedHitObjects)
|
||||||
simulateHit(nested);
|
simulateHit(nested, ref attributes);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,8 +156,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
{
|
{
|
||||||
int oldScoreIncrease = scoreIncrease;
|
int oldScoreIncrease = scoreIncrease;
|
||||||
|
|
||||||
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
scoreIncrease += scoreIncrease / 35 * 2 * (difficultyPeppyStars + 1) * (Math.Min(100, combo) / 10);
|
||||||
scoreIncrease += (int)(scoreIncrease / 35 * 2 * (difficultyPeppyStars + 1) * modMultiplier) * (Math.Min(100, combo) / 10);
|
|
||||||
|
|
||||||
if (hitObject is Swell)
|
if (hitObject is Swell)
|
||||||
{
|
{
|
||||||
@ -185,15 +181,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
scoreIncrease -= comboScoreIncrease;
|
scoreIncrease -= comboScoreIncrease;
|
||||||
|
|
||||||
if (addScoreComboMultiplier)
|
if (addScoreComboMultiplier)
|
||||||
ComboScore += comboScoreIncrease;
|
attributes.ComboScore += comboScoreIncrease;
|
||||||
|
|
||||||
if (isBonus)
|
if (isBonus)
|
||||||
{
|
{
|
||||||
legacyBonusScore += scoreIncrease;
|
legacyBonusScore += scoreIncrease;
|
||||||
modernBonusScore += Judgement.ToNumericResult(bonusResult);
|
standardisedBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
AccuracyScore += scoreIncrease;
|
attributes.AccuracyScore += scoreIncrease;
|
||||||
|
|
||||||
if (increaseCombo)
|
if (increaseCombo)
|
||||||
combo++;
|
combo++;
|
||||||
|
37
osu.Game.Rulesets.Taiko/Edit/DrawableTaikoEditorRuleset.cs
Normal file
37
osu.Game.Rulesets.Taiko/Edit/DrawableTaikoEditorRuleset.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Edit
|
||||||
|
{
|
||||||
|
public partial class DrawableTaikoEditorRuleset : DrawableTaikoRuleset, ISupportConstantAlgorithmToggle
|
||||||
|
{
|
||||||
|
public BindableBool ShowSpeedChanges { get; set; } = new BindableBool();
|
||||||
|
|
||||||
|
public DrawableTaikoEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||||
|
: base(ruleset, beatmap, mods)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Overlapping : ScrollVisualisationMethod.Constant, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override double ComputeTimeRange()
|
||||||
|
{
|
||||||
|
// Adjust when we're using constant algorithm to not be sluggish.
|
||||||
|
double multiplier = ShowSpeedChanges.Value ? 1 : 4;
|
||||||
|
return base.ComputeTimeRange() / multiplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,14 +2,17 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Edit.Tools;
|
using osu.Game.Rulesets.Edit.Tools;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Edit
|
namespace osu.Game.Rulesets.Taiko.Edit
|
||||||
{
|
{
|
||||||
public partial class TaikoHitObjectComposer : HitObjectComposer<TaikoHitObject>
|
public partial class TaikoHitObjectComposer : ScrollingHitObjectComposer<TaikoHitObject>
|
||||||
{
|
{
|
||||||
protected override bool ApplyHorizontalCentering => false;
|
protected override bool ApplyHorizontalCentering => false;
|
||||||
|
|
||||||
@ -25,6 +28,9 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
|||||||
new SwellCompositionTool()
|
new SwellCompositionTool()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
protected override DrawableRuleset<TaikoHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods) =>
|
||||||
|
new DrawableTaikoEditorRuleset(ruleset, beatmap, mods);
|
||||||
|
|
||||||
protected override ComposeBlueprintContainer CreateBlueprintContainer()
|
protected override ComposeBlueprintContainer CreateBlueprintContainer()
|
||||||
=> new TaikoBlueprintContainer(this);
|
=> new TaikoBlueprintContainer(this);
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
public partial class LegacyCirclePiece : CompositeDrawable, IHasAccentColour
|
public partial class LegacyCirclePiece : CompositeDrawable, IHasAccentColour
|
||||||
{
|
{
|
||||||
|
private static readonly Vector2 circle_piece_size = new Vector2(128);
|
||||||
|
|
||||||
private Drawable backgroundLayer = null!;
|
private Drawable backgroundLayer = null!;
|
||||||
private Drawable? foregroundLayer;
|
private Drawable? foregroundLayer;
|
||||||
|
|
||||||
@ -52,9 +54,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
|
|
||||||
string prefix = ((drawableHitObject.HitObject as TaikoStrongableHitObject)?.IsStrong ?? false) ? big_hit : normal_hit;
|
string prefix = ((drawableHitObject.HitObject as TaikoStrongableHitObject)?.IsStrong ?? false) ? big_hit : normal_hit;
|
||||||
|
|
||||||
return skin.GetAnimation($"{prefix}{lookup}", true, false) ??
|
return skin.GetAnimation($"{prefix}{lookup}", true, false, maxSize: circle_piece_size) ??
|
||||||
// fallback to regular size if "big" version doesn't exist.
|
// fallback to regular size if "big" version doesn't exist.
|
||||||
skin.GetAnimation($"{normal_hit}{lookup}", true, false);
|
skin.GetAnimation($"{normal_hit}{lookup}", true, false, maxSize: circle_piece_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
|
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
|
||||||
@ -96,7 +98,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
// Not all skins (including the default osu-stable) have similar sizes for "hitcircle" and "hitcircleoverlay".
|
// Not all skins (including the default osu-stable) have similar sizes for "hitcircle" and "hitcircleoverlay".
|
||||||
// This ensures they are scaled relative to each other but also match the expected DrawableHit size.
|
// This ensures they are scaled relative to each other but also match the expected DrawableHit size.
|
||||||
foreach (var c in InternalChildren)
|
foreach (var c in InternalChildren)
|
||||||
c.Scale = new Vector2(DrawHeight / 128);
|
c.Scale = new Vector2(DrawHeight / circle_piece_size.Y);
|
||||||
|
|
||||||
if (foregroundLayer is IFramedAnimation animatableForegroundLayer)
|
if (foregroundLayer is IFramedAnimation animatableForegroundLayer)
|
||||||
animateForegroundLayer(animatableForegroundLayer);
|
animateForegroundLayer(animatableForegroundLayer);
|
||||||
|
@ -34,6 +34,7 @@ using osu.Game.Screens.Ranking.Statistics;
|
|||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osu.Game.Rulesets.Configuration;
|
using osu.Game.Rulesets.Configuration;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Rulesets.Scoring.Legacy;
|
||||||
using osu.Game.Rulesets.Taiko.Configuration;
|
using osu.Game.Rulesets.Taiko.Configuration;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko
|
namespace osu.Game.Rulesets.Taiko
|
||||||
|
@ -35,8 +35,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
|
|
||||||
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
|
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
|
||||||
|
|
||||||
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
|
|
||||||
|
|
||||||
protected override bool UserScrollSpeedAdjustment => false;
|
protected override bool UserScrollSpeedAdjustment => false;
|
||||||
|
|
||||||
private SkinnableDrawable scroller;
|
private SkinnableDrawable scroller;
|
||||||
@ -45,6 +43,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
: base(ruleset, beatmap, mods)
|
: base(ruleset, beatmap, mods)
|
||||||
{
|
{
|
||||||
Direction.Value = ScrollingDirection.Left;
|
Direction.Value = ScrollingDirection.Left;
|
||||||
|
VisualisationMethod = ScrollVisualisationMethod.Overlapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -65,6 +64,11 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
|
TimeRange.Value = ComputeTimeRange();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual double ComputeTimeRange()
|
||||||
|
{
|
||||||
// Taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
|
// Taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
|
||||||
const float scroll_rate = 10;
|
const float scroll_rate = 10;
|
||||||
|
|
||||||
@ -73,7 +77,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
// We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default.
|
// We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default.
|
||||||
float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT);
|
float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT);
|
||||||
|
|
||||||
TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
|
return (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateAfterChildren()
|
protected override void UpdateAfterChildren()
|
||||||
|
@ -621,6 +621,38 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInvalidBankDefaultsToNormal()
|
||||||
|
{
|
||||||
|
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||||
|
|
||||||
|
using (var resStream = TestResources.OpenResource("invalid-bank.osu"))
|
||||||
|
using (var stream = new LineBufferedReader(resStream))
|
||||||
|
{
|
||||||
|
var hitObjects = decoder.Decode(stream).HitObjects;
|
||||||
|
|
||||||
|
assertObjectHasBanks(hitObjects[0], HitSampleInfo.BANK_DRUM);
|
||||||
|
assertObjectHasBanks(hitObjects[1], HitSampleInfo.BANK_NORMAL);
|
||||||
|
assertObjectHasBanks(hitObjects[2], HitSampleInfo.BANK_SOFT);
|
||||||
|
assertObjectHasBanks(hitObjects[3], HitSampleInfo.BANK_DRUM);
|
||||||
|
assertObjectHasBanks(hitObjects[4], HitSampleInfo.BANK_NORMAL);
|
||||||
|
|
||||||
|
assertObjectHasBanks(hitObjects[5], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_DRUM);
|
||||||
|
assertObjectHasBanks(hitObjects[6], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_NORMAL);
|
||||||
|
assertObjectHasBanks(hitObjects[7], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_SOFT);
|
||||||
|
assertObjectHasBanks(hitObjects[8], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_DRUM);
|
||||||
|
assertObjectHasBanks(hitObjects[9], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_NORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void assertObjectHasBanks(HitObject hitObject, string normalBank, string? additionsBank = null)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(normalBank, hitObject.Samples[0].Bank);
|
||||||
|
|
||||||
|
if (additionsBank != null)
|
||||||
|
Assert.AreEqual(additionsBank, hitObject.Samples[1].Bank);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestFallbackDecoderForCorruptedHeader()
|
public void TestFallbackDecoderForCorruptedHeader()
|
||||||
{
|
{
|
||||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestSingleSpan()
|
public void TestSingleSpan()
|
||||||
{
|
{
|
||||||
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null).ToArray();
|
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1).ToArray();
|
||||||
|
|
||||||
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
|
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
|
||||||
Assert.That(events[0].Time, Is.EqualTo(start_time));
|
Assert.That(events[0].Time, Is.EqualTo(start_time));
|
||||||
@ -31,7 +31,7 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestRepeat()
|
public void TestRepeat()
|
||||||
{
|
{
|
||||||
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null).ToArray();
|
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2).ToArray();
|
||||||
|
|
||||||
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
|
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
|
||||||
Assert.That(events[0].Time, Is.EqualTo(start_time));
|
Assert.That(events[0].Time, Is.EqualTo(start_time));
|
||||||
@ -52,7 +52,7 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestNonEvenTicks()
|
public void TestNonEvenTicks()
|
||||||
{
|
{
|
||||||
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null).ToArray();
|
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2).ToArray();
|
||||||
|
|
||||||
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
|
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
|
||||||
Assert.That(events[0].Time, Is.EqualTo(start_time));
|
Assert.That(events[0].Time, Is.EqualTo(start_time));
|
||||||
@ -83,12 +83,12 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestLegacyLastTickOffset()
|
public void TestLastTickOffset()
|
||||||
{
|
{
|
||||||
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100).ToArray();
|
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1).ToArray();
|
||||||
|
|
||||||
Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick));
|
Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LastTick));
|
||||||
Assert.That(events[2].Time, Is.EqualTo(900));
|
Assert.That(events[2].Time, Is.EqualTo(span_duration + SliderEventGenerator.LAST_TICK_OFFSET));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -97,7 +97,7 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
const double velocity = 5;
|
const double velocity = 5;
|
||||||
const double min_distance = velocity * 10;
|
const double min_distance = velocity * 10;
|
||||||
|
|
||||||
var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0).ToArray();
|
var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2).ToArray();
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
|
@ -44,17 +44,23 @@ namespace osu.Game.Tests.Database
|
|||||||
createFile(subdirectory2, Path.Combine("beatmap5", "beatmap.osu"));
|
createFile(subdirectory2, Path.Combine("beatmap5", "beatmap.osu"));
|
||||||
createFile(subdirectory2, Path.Combine("beatmap6", "beatmap.osu"));
|
createFile(subdirectory2, Path.Combine("beatmap6", "beatmap.osu"));
|
||||||
|
|
||||||
|
// songs subdirectory with random file
|
||||||
|
var subdirectory3 = songsStorage.GetStorageForDirectory("subdirectory3");
|
||||||
|
createFile(subdirectory3, "silly readme.txt");
|
||||||
|
createFile(subdirectory3, Path.Combine("beatmap7", "beatmap.osu"));
|
||||||
|
|
||||||
// empty songs subdirectory
|
// empty songs subdirectory
|
||||||
songsStorage.GetStorageForDirectory("subdirectory3");
|
songsStorage.GetStorageForDirectory("subdirectory3");
|
||||||
|
|
||||||
string[] paths = importer.GetStableImportPaths(songsStorage).ToArray();
|
string[] paths = importer.GetStableImportPaths(songsStorage).ToArray();
|
||||||
Assert.That(paths.Length, Is.EqualTo(6));
|
Assert.That(paths.Length, Is.EqualTo(7));
|
||||||
Assert.That(paths.Contains(songsStorage.GetFullPath("beatmap1")));
|
Assert.That(paths.Contains(songsStorage.GetFullPath("beatmap1")));
|
||||||
Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory", "beatmap2"))));
|
Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory", "beatmap2"))));
|
||||||
Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory", "beatmap3"))));
|
Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory", "beatmap3"))));
|
||||||
Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory", "sub-subdirectory", "beatmap4"))));
|
Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory", "sub-subdirectory", "beatmap4"))));
|
||||||
Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory2", "beatmap5"))));
|
Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory2", "beatmap5"))));
|
||||||
Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory2", "beatmap6"))));
|
Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory2", "beatmap6"))));
|
||||||
|
Assert.That(paths.Contains(songsStorage.GetFullPath(Path.Combine("subdirectory3", "beatmap7"))));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void createFile(Storage storage, string path)
|
static void createFile(Storage storage, string path)
|
||||||
|
Binary file not shown.
19
osu.Game.Tests/Resources/invalid-bank.osu
Normal file
19
osu.Game.Tests/Resources/invalid-bank.osu
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
osu file format v14
|
||||||
|
|
||||||
|
[General]
|
||||||
|
SampleSet: Normal
|
||||||
|
|
||||||
|
[TimingPoints]
|
||||||
|
0,500,4,3,0,100,1,0
|
||||||
|
|
||||||
|
[HitObjects]
|
||||||
|
256,192,1000,5,0,0:0:0:0:
|
||||||
|
256,192,2000,1,0,1:0:0:0:
|
||||||
|
256,192,3000,1,0,2:0:0:0:
|
||||||
|
256,192,4000,1,0,3:0:0:0:
|
||||||
|
256,192,5000,1,0,42:0:0:0:
|
||||||
|
256,192,6000,5,4,0:0:0:0:
|
||||||
|
256,192,7000,1,4,0:1:0:0:
|
||||||
|
256,192,8000,1,4,0:2:0:0:
|
||||||
|
256,192,9000,1,4,0:3:0:0:
|
||||||
|
256,192,10000,1,4,0:42:0:0:
|
@ -45,9 +45,9 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
|||||||
[TestCase(ScoringMode.Standardised, HitResult.Meh, 116_667)]
|
[TestCase(ScoringMode.Standardised, HitResult.Meh, 116_667)]
|
||||||
[TestCase(ScoringMode.Standardised, HitResult.Ok, 233_338)]
|
[TestCase(ScoringMode.Standardised, HitResult.Ok, 233_338)]
|
||||||
[TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)]
|
[TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.Meh, 0)]
|
[TestCase(ScoringMode.Classic, HitResult.Meh, 11_670)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.Ok, 2)]
|
[TestCase(ScoringMode.Classic, HitResult.Ok, 23_341)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.Great, 36)]
|
[TestCase(ScoringMode.Classic, HitResult.Great, 100_033)]
|
||||||
public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
|
public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
|
||||||
{
|
{
|
||||||
scoreProcessor.ApplyBeatmap(beatmap);
|
scoreProcessor.ApplyBeatmap(beatmap);
|
||||||
@ -84,17 +84,17 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
|||||||
[TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)]
|
[TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)]
|
||||||
[TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)]
|
[TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)]
|
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 4)]
|
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 7_975)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 15)]
|
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 15_949)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 53)]
|
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 30_398)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 140)]
|
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 49_546)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 140)]
|
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 49_546)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
|
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 11)]
|
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 54_189)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
|
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 9)]
|
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 49_289)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 36)]
|
[TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 100_003)]
|
||||||
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 36)]
|
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 100_015)]
|
||||||
public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore)
|
public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore)
|
||||||
{
|
{
|
||||||
var minResult = new TestJudgement(hitResult).MinResult;
|
var minResult = new TestJudgement(hitResult).MinResult;
|
||||||
|
@ -27,7 +27,7 @@ namespace osu.Game.Tests.Skins.IO
|
|||||||
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk"));
|
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk"));
|
||||||
|
|
||||||
// When the import filename doesn't match, it should be appended (and update the skin.ini).
|
// When the import filename doesn't match, it should be appended (and update the skin.ini).
|
||||||
assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu);
|
assertCorrectMetadata(import1, "test skin [skin]", "skinner", 1.0m, osu);
|
||||||
});
|
});
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -36,7 +36,7 @@ namespace osu.Game.Tests.Skins.IO
|
|||||||
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner", iniFilename: "Skin.InI"), "skin.osk"));
|
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner", iniFilename: "Skin.InI"), "skin.osk"));
|
||||||
|
|
||||||
// When the import filename doesn't match, it should be appended (and update the skin.ini).
|
// When the import filename doesn't match, it should be appended (and update the skin.ini).
|
||||||
assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu);
|
assertCorrectMetadata(import1, "test skin [skin]", "skinner", 1.0m, osu);
|
||||||
});
|
});
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -45,7 +45,7 @@ namespace osu.Game.Tests.Skins.IO
|
|||||||
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner", includeSectionHeader: false), "skin.osk"));
|
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner", includeSectionHeader: false), "skin.osk"));
|
||||||
|
|
||||||
// When the import filename doesn't match, it should be appended (and update the skin.ini).
|
// When the import filename doesn't match, it should be appended (and update the skin.ini).
|
||||||
assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu);
|
assertCorrectMetadata(import1, "test skin [skin]", "skinner", 1.0m, osu);
|
||||||
});
|
});
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -54,7 +54,7 @@ namespace osu.Game.Tests.Skins.IO
|
|||||||
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "test skin.osk"));
|
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "test skin.osk"));
|
||||||
|
|
||||||
// When the import filename matches it shouldn't be appended.
|
// When the import filename matches it shouldn't be appended.
|
||||||
assertCorrectMetadata(import1, "test skin", "skinner", osu);
|
assertCorrectMetadata(import1, "test skin", "skinner", 1.0m, osu);
|
||||||
});
|
});
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -63,7 +63,7 @@ namespace osu.Game.Tests.Skins.IO
|
|||||||
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithNonIniFile(), "test skin.osk"));
|
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithNonIniFile(), "test skin.osk"));
|
||||||
|
|
||||||
// When the import filename matches it shouldn't be appended.
|
// When the import filename matches it shouldn't be appended.
|
||||||
assertCorrectMetadata(import1, "test skin", "Unknown", osu);
|
assertCorrectMetadata(import1, "test skin", "Unknown", SkinConfiguration.LATEST_VERSION, osu);
|
||||||
});
|
});
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -72,7 +72,7 @@ namespace osu.Game.Tests.Skins.IO
|
|||||||
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createEmptyOsk(), "test skin.osk"));
|
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createEmptyOsk(), "test skin.osk"));
|
||||||
|
|
||||||
// When the import filename matches it shouldn't be appended.
|
// When the import filename matches it shouldn't be appended.
|
||||||
assertCorrectMetadata(import1, "test skin", "Unknown", osu);
|
assertCorrectMetadata(import1, "test skin", "Unknown", SkinConfiguration.LATEST_VERSION, osu);
|
||||||
});
|
});
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -102,7 +102,7 @@ namespace osu.Game.Tests.Skins.IO
|
|||||||
public Task TestImportUpperCasedOskArchive() => runSkinTest(async osu =>
|
public Task TestImportUpperCasedOskArchive() => runSkinTest(async osu =>
|
||||||
{
|
{
|
||||||
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "name 1.OsK"));
|
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "name 1.OsK"));
|
||||||
assertCorrectMetadata(import1, "name 1", "author 1", osu);
|
assertCorrectMetadata(import1, "name 1", "author 1", 1.0m, osu);
|
||||||
|
|
||||||
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "name 1.oSK"));
|
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "name 1.oSK"));
|
||||||
|
|
||||||
@ -115,14 +115,14 @@ namespace osu.Game.Tests.Skins.IO
|
|||||||
MemoryStream exportStream = new MemoryStream();
|
MemoryStream exportStream = new MemoryStream();
|
||||||
|
|
||||||
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "custom.osk"));
|
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "custom.osk"));
|
||||||
assertCorrectMetadata(import1, "name 1 [custom]", "author 1", osu);
|
assertCorrectMetadata(import1, "name 1 [custom]", "author 1", 1.0m, osu);
|
||||||
|
|
||||||
await new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportToStreamAsync(import1, exportStream);
|
await new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportToStreamAsync(import1, exportStream);
|
||||||
|
|
||||||
string exportFilename = import1.GetDisplayString();
|
string exportFilename = import1.GetDisplayString();
|
||||||
|
|
||||||
var import2 = await loadSkinIntoOsu(osu, new ImportTask(exportStream, $"{exportFilename}.osk"));
|
var import2 = await loadSkinIntoOsu(osu, new ImportTask(exportStream, $"{exportFilename}.osk"));
|
||||||
assertCorrectMetadata(import2, "name 1 [custom]", "author 1", osu);
|
assertCorrectMetadata(import2, "name 1 [custom]", "author 1", 1.0m, osu);
|
||||||
|
|
||||||
assertImportedOnce(import1, import2);
|
assertImportedOnce(import1, import2);
|
||||||
});
|
});
|
||||||
@ -133,14 +133,14 @@ namespace osu.Game.Tests.Skins.IO
|
|||||||
MemoryStream exportStream = new MemoryStream();
|
MemoryStream exportStream = new MemoryStream();
|
||||||
|
|
||||||
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 『1』", "author 1"), "custom.osk"));
|
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 『1』", "author 1"), "custom.osk"));
|
||||||
assertCorrectMetadata(import1, "name 『1』 [custom]", "author 1", osu);
|
assertCorrectMetadata(import1, "name 『1』 [custom]", "author 1", 1.0m, osu);
|
||||||
|
|
||||||
await new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportToStreamAsync(import1, exportStream);
|
await new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportToStreamAsync(import1, exportStream);
|
||||||
|
|
||||||
string exportFilename = import1.GetDisplayString().GetValidFilename();
|
string exportFilename = import1.GetDisplayString().GetValidFilename();
|
||||||
|
|
||||||
var import2 = await loadSkinIntoOsu(osu, new ImportTask(exportStream, $"{exportFilename}.osk"));
|
var import2 = await loadSkinIntoOsu(osu, new ImportTask(exportStream, $"{exportFilename}.osk"));
|
||||||
assertCorrectMetadata(import2, "name 『1』 [custom]", "author 1", osu);
|
assertCorrectMetadata(import2, "name 『1』 [custom]", "author 1", 1.0m, osu);
|
||||||
});
|
});
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -150,7 +150,7 @@ namespace osu.Game.Tests.Skins.IO
|
|||||||
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1"), batchImport);
|
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1"), batchImport);
|
||||||
|
|
||||||
assertImportedOnce(import1, import2);
|
assertImportedOnce(import1, import2);
|
||||||
assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", osu);
|
assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", 1.0m, osu);
|
||||||
});
|
});
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -183,8 +183,8 @@ namespace osu.Game.Tests.Skins.IO
|
|||||||
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin v2.1", "skinner"), "skin.osk"));
|
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin v2.1", "skinner"), "skin.osk"));
|
||||||
|
|
||||||
assertImportedBoth(import1, import2);
|
assertImportedBoth(import1, import2);
|
||||||
assertCorrectMetadata(import1, "test skin v2 [skin]", "skinner", osu);
|
assertCorrectMetadata(import1, "test skin v2 [skin]", "skinner", 1.0m, osu);
|
||||||
assertCorrectMetadata(import2, "test skin v2.1 [skin]", "skinner", osu);
|
assertCorrectMetadata(import2, "test skin v2.1 [skin]", "skinner", 1.0m, osu);
|
||||||
});
|
});
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -194,8 +194,8 @@ namespace osu.Game.Tests.Skins.IO
|
|||||||
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 2"));
|
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 2"));
|
||||||
|
|
||||||
assertImportedBoth(import1, import2);
|
assertImportedBoth(import1, import2);
|
||||||
assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", osu);
|
assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", 1.0m, osu);
|
||||||
assertCorrectMetadata(import2, "name 1 [my custom skin 2]", "author 1", osu);
|
assertCorrectMetadata(import2, "name 1 [my custom skin 2]", "author 1", 1.0m, osu);
|
||||||
});
|
});
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -264,7 +264,7 @@ namespace osu.Game.Tests.Skins.IO
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private void assertCorrectMetadata(Live<SkinInfo> import1, string name, string creator, OsuGameBase osu)
|
private void assertCorrectMetadata(Live<SkinInfo> import1, string name, string creator, decimal version, OsuGameBase osu)
|
||||||
{
|
{
|
||||||
import1.PerformRead(i =>
|
import1.PerformRead(i =>
|
||||||
{
|
{
|
||||||
@ -276,6 +276,7 @@ namespace osu.Game.Tests.Skins.IO
|
|||||||
|
|
||||||
Assert.That(instance.Configuration.SkinInfo.Name, Is.EqualTo(name));
|
Assert.That(instance.Configuration.SkinInfo.Name, Is.EqualTo(name));
|
||||||
Assert.That(instance.Configuration.SkinInfo.Creator, Is.EqualTo(creator));
|
Assert.That(instance.Configuration.SkinInfo.Creator, Is.EqualTo(creator));
|
||||||
|
Assert.That(instance.Configuration.LegacyVersion, Is.EqualTo(version));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +52,9 @@ namespace osu.Game.Tests.Skins
|
|||||||
// Covers player avatar and flag.
|
// Covers player avatar and flag.
|
||||||
"Archives/modified-argon-20230305.osk",
|
"Archives/modified-argon-20230305.osk",
|
||||||
// Covers key counters
|
// Covers key counters
|
||||||
"Archives/modified-argon-pro-20230618.osk"
|
"Archives/modified-argon-pro-20230618.osk",
|
||||||
|
// Covers "Argon" health display
|
||||||
|
"Archives/modified-argon-pro-20231001.osk"
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -125,7 +125,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("track is not virtual", () => Beatmap.Value.Track is not TrackVirtual);
|
AddAssert("track is not virtual", () => Beatmap.Value.Track is not TrackVirtual);
|
||||||
AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000);
|
AddUntilStep("track length changed", () => Beatmap.Value.Track.Length > 60000);
|
||||||
|
|
||||||
AddStep("test play", () => Editor.TestGameplay());
|
AddStep("test play", () => Editor.TestGameplay());
|
||||||
|
|
||||||
|
@ -67,6 +67,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
private Player loadPlayerFor(RulesetInfo rulesetInfo)
|
private Player loadPlayerFor(RulesetInfo rulesetInfo)
|
||||||
{
|
{
|
||||||
|
// if a player screen is present already, we must exit that before loading another one,
|
||||||
|
// otherwise it'll crash on SpectatorClient.BeginPlaying being called while client is in "playing" state already.
|
||||||
|
if (Stack.CurrentScreen is Player)
|
||||||
|
Stack.Exit();
|
||||||
|
|
||||||
Ruleset.Value = rulesetInfo;
|
Ruleset.Value = rulesetInfo;
|
||||||
var ruleset = rulesetInfo.CreateInstance();
|
var ruleset = rulesetInfo.CreateInstance();
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user