mirror of
https://github.com/ppy/osu.git
synced 2024-12-15 08:13:31 +08:00
Merge branch 'master' into editor/checks/delayed-hitsounds
This commit is contained in:
commit
12611a09f1
@ -50,7 +50,7 @@ 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.
|
||||||
|
|
||||||
When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
|
When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) plugin installed.
|
||||||
|
|
||||||
### Downloading the source code
|
### Downloading the source code
|
||||||
|
|
||||||
|
@ -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.823.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.914.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>
|
||||||
|
@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
|
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
|
||||||
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
||||||
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
|
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
|
||||||
AddAssert("default slider velocity", () => lastObject.SliderVelocityBindable.IsDefault);
|
AddAssert("default slider velocity", () => lastObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
addPlacementSteps(times, positions);
|
addPlacementSteps(times, positions);
|
||||||
addPathCheckStep(times, positions);
|
addPathCheckStep(times, positions);
|
||||||
|
|
||||||
AddAssert("slider velocity changed", () => !lastObject.SliderVelocityBindable.IsDefault);
|
AddAssert("slider velocity changed", () => !lastObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -108,11 +108,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
double[] times = { 100, 300 };
|
double[] times = { 100, 300 };
|
||||||
float[] positions = { 200, 300 };
|
float[] positions = { 200, 300 };
|
||||||
addBlueprintStep(times, positions);
|
addBlueprintStep(times, positions);
|
||||||
AddAssert("default slider velocity", () => hitObject.SliderVelocityBindable.IsDefault);
|
AddAssert("default slider velocity", () => hitObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||||
|
|
||||||
addDragStartStep(times[1], positions[1]);
|
addDragStartStep(times[1], positions[1]);
|
||||||
AddMouseMoveStep(times[1], 400);
|
AddMouseMoveStep(times[1], 400);
|
||||||
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityBindable.IsDefault);
|
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
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>
|
||||||
|
@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
|||||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||||
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
|
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
|
||||||
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
||||||
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1
|
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
|
||||||
}.Yield();
|
}.Yield();
|
||||||
|
|
||||||
case IHasDuration endTime:
|
case IHasDuration endTime:
|
||||||
|
@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||||
{
|
{
|
||||||
// The SV setting may need to be changed for the current path.
|
// The SV setting may need to be changed for the current path.
|
||||||
var svBindable = hitObject.SliderVelocityBindable;
|
var svBindable = hitObject.SliderVelocityMultiplierBindable;
|
||||||
double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
|
double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
|
||||||
double requiredVelocity = path.ComputeRequiredVelocity();
|
double requiredVelocity = path.ComputeRequiredVelocity();
|
||||||
|
|
||||||
|
@ -28,17 +28,17 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
|
|
||||||
public int RepeatCount { get; set; }
|
public int RepeatCount { get; set; }
|
||||||
|
|
||||||
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
|
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
|
||||||
{
|
{
|
||||||
Precision = 0.01,
|
Precision = 0.01,
|
||||||
MinValue = 0.1,
|
MinValue = 0.1,
|
||||||
MaxValue = 10
|
MaxValue = 10
|
||||||
};
|
};
|
||||||
|
|
||||||
public double SliderVelocity
|
public double SliderVelocityMultiplier
|
||||||
{
|
{
|
||||||
get => SliderVelocityBindable.Value;
|
get => SliderVelocityMultiplierBindable.Value;
|
||||||
set => SliderVelocityBindable.Value = value;
|
set => SliderVelocityMultiplierBindable.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
@ -48,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
private double tickDistanceFactor;
|
private double tickDistanceFactor;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public double Velocity => velocityFactor * SliderVelocity;
|
public double Velocity => velocityFactor * SliderVelocityMultiplier;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public double TickDistance => tickDistanceFactor * SliderVelocity;
|
public double TickDistance => tickDistanceFactor * SliderVelocityMultiplier;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The length of one span of this <see cref="JuiceStream"/>.
|
/// The length of one span of this <see cref="JuiceStream"/>.
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Mods;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Mania.Replays;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Replays;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||||
|
{
|
||||||
|
public partial class TestSceneManiaModDoubleTime : ModTestScene
|
||||||
|
{
|
||||||
|
private const double offset = 18;
|
||||||
|
|
||||||
|
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHitWindowWithoutDoubleTime() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value != 1,
|
||||||
|
Autoplay = false,
|
||||||
|
Beatmap = new Beatmap
|
||||||
|
{
|
||||||
|
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
|
||||||
|
Difficulty = { OverallDifficulty = 10 },
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new Note { StartTime = 1000 }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReplayFrames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new ManiaReplayFrame(1000 + offset, ManiaAction.Key1)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHitWindowWithDoubleTime() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new ManiaModDoubleTime(),
|
||||||
|
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value == 1,
|
||||||
|
Autoplay = false,
|
||||||
|
Beatmap = new Beatmap
|
||||||
|
{
|
||||||
|
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
|
||||||
|
Difficulty = { OverallDifficulty = 10 },
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new Note { StartTime = 1000 }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReplayFrames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new ManiaReplayFrame(1000 + offset, ManiaAction.Key1)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
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>
|
||||||
|
@ -10,10 +10,11 @@ using System.Linq;
|
|||||||
using osu.Framework.Extensions.EnumExtensions;
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Objects;
|
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||||
@ -50,10 +51,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
|
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
|
||||||
|
|
||||||
double beatLength;
|
double beatLength;
|
||||||
if (hitObject.LegacyBpmMultiplier.HasValue)
|
|
||||||
beatLength = timingPoint.BeatLength * hitObject.LegacyBpmMultiplier.Value;
|
if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
||||||
else if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
beatLength = LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(hasSliderVelocity, timingPoint, ManiaRuleset.SHORT_NAME);
|
||||||
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
|
|
||||||
else
|
else
|
||||||
beatLength = timingPoint.BeatLength;
|
beatLength = timingPoint.BeatLength;
|
||||||
|
|
||||||
|
47
osu.Game.Rulesets.Mania/Mods/IManiaRateAdjustmentMod.cs
Normal file
47
osu.Game.Rulesets.Mania/Mods/IManiaRateAdjustmentMod.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Mania.Scoring;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Mods
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// May be attached to rate-adjustment mods to adjust hit windows adjust relative to gameplay rate.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Historically, in osu!mania, hit windows are expected to adjust relative to the gameplay rate such that the real-world hit window remains the same.
|
||||||
|
/// </remarks>
|
||||||
|
public interface IManiaRateAdjustmentMod : IApplicableToDifficulty, IApplicableToHitObject
|
||||||
|
{
|
||||||
|
BindableNumber<double> SpeedChange { get; }
|
||||||
|
|
||||||
|
HitWindows HitWindows { get; set; }
|
||||||
|
|
||||||
|
void IApplicableToDifficulty.ApplyToDifficulty(BeatmapDifficulty difficulty)
|
||||||
|
{
|
||||||
|
HitWindows = new ManiaHitWindows(SpeedChange.Value);
|
||||||
|
HitWindows.SetDifficulty(difficulty.OverallDifficulty);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
|
||||||
|
{
|
||||||
|
switch (hitObject)
|
||||||
|
{
|
||||||
|
case Note:
|
||||||
|
hitObject.HitWindows = HitWindows;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HoldNote hold:
|
||||||
|
hold.Head.HitWindows = HitWindows;
|
||||||
|
hold.Tail.HitWindows = HitWindows;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,14 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Rulesets.Mania.Scoring;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Mods
|
namespace osu.Game.Rulesets.Mania.Mods
|
||||||
{
|
{
|
||||||
public class ManiaModDaycore : ModDaycore
|
public class ManiaModDaycore : ModDaycore, IManiaRateAdjustmentMod
|
||||||
{
|
{
|
||||||
|
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Rulesets.Mania.Scoring;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Mods
|
namespace osu.Game.Rulesets.Mania.Mods
|
||||||
{
|
{
|
||||||
public class ManiaModDoubleTime : ModDoubleTime
|
public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod
|
||||||
{
|
{
|
||||||
|
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Rulesets.Mania.Scoring;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Mods
|
namespace osu.Game.Rulesets.Mania.Mods
|
||||||
{
|
{
|
||||||
public class ManiaModHalfTime : ModHalfTime
|
public class ManiaModHalfTime : ModHalfTime, IManiaRateAdjustmentMod
|
||||||
{
|
{
|
||||||
|
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,14 @@
|
|||||||
// 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.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Mania.Scoring;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Mods
|
namespace osu.Game.Rulesets.Mania.Mods
|
||||||
{
|
{
|
||||||
public class ManiaModNightcore : ModNightcore<ManiaHitObject>
|
public class ManiaModNightcore : ModNightcore<ManiaHitObject>, IManiaRateAdjustmentMod
|
||||||
{
|
{
|
||||||
|
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,25 @@
|
|||||||
// 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.Linq;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Scoring
|
namespace osu.Game.Rulesets.Mania.Scoring
|
||||||
{
|
{
|
||||||
public class ManiaHitWindows : HitWindows
|
public class ManiaHitWindows : HitWindows
|
||||||
{
|
{
|
||||||
|
private readonly double multiplier;
|
||||||
|
|
||||||
|
public ManiaHitWindows()
|
||||||
|
: this(1)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManiaHitWindows(double multiplier)
|
||||||
|
{
|
||||||
|
this.multiplier = multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
public override bool IsHitResultAllowed(HitResult result)
|
public override bool IsHitResultAllowed(HitResult result)
|
||||||
{
|
{
|
||||||
switch (result)
|
switch (result)
|
||||||
@ -22,5 +35,12 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override DifficultyRange[] GetRanges() => base.GetRanges().Select(r =>
|
||||||
|
new DifficultyRange(
|
||||||
|
r.Result,
|
||||||
|
r.Min * multiplier,
|
||||||
|
r.Average * multiplier,
|
||||||
|
r.Max * multiplier)).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
|||||||
Lookup = lookup;
|
Lookup = lookup;
|
||||||
ColumnIndex = columnIndex;
|
ColumnIndex = columnIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString() => $"[{nameof(ManiaSkinConfigurationLookup)} lookup:{Lookup} col:{ColumnIndex}]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
|||||||
new PathControlPoint(new Vector2(0, 6.25f))
|
new PathControlPoint(new Vector2(0, 6.25f))
|
||||||
}),
|
}),
|
||||||
RepeatCount = 1,
|
RepeatCount = 1,
|
||||||
SliderVelocity = 10
|
SliderVelocityMultiplier = 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
[TestCase("basic")]
|
[TestCase("basic")]
|
||||||
[TestCase("colinear-perfect-curve")]
|
[TestCase("colinear-perfect-curve")]
|
||||||
[TestCase("slider-ticks")]
|
[TestCase("slider-ticks")]
|
||||||
|
[TestCase("slider-ticks-edge-case")]
|
||||||
[TestCase("repeat-slider")]
|
[TestCase("repeat-slider")]
|
||||||
[TestCase("uneven-repeat-slider")]
|
[TestCase("uneven-repeat-slider")]
|
||||||
[TestCase("old-stacking")]
|
[TestCase("old-stacking")]
|
||||||
|
@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
{
|
{
|
||||||
if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current) != false)
|
if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current, HitResult.Great) == ClickAction.Hit)
|
||||||
{
|
{
|
||||||
// force success
|
// force success
|
||||||
ApplyResult(r => r.Type = HitResult.Great);
|
ApplyResult(r => r.Type = HitResult.Great);
|
||||||
|
@ -13,6 +13,7 @@ using osu.Game.Rulesets.Objects.Types;
|
|||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
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.Rulesets.Scoring;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private float? alphaAtMiss;
|
private float? alphaAtMiss;
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestHitCircleClassicMod()
|
public void TestHitCircleClassicModMiss()
|
||||||
{
|
{
|
||||||
AddStep("Create hit circle", () =>
|
AddStep("Create hit circle", () =>
|
||||||
{
|
{
|
||||||
@ -61,8 +62,27 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
|
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No early fade is expected to be applied if the hit circle has been hit.
|
||||||
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
public void TestHitCircleNoMod()
|
public void TestHitCircleClassicModHit()
|
||||||
|
{
|
||||||
|
TestDrawableHitCircle circle = null!;
|
||||||
|
|
||||||
|
AddStep("Create hit circle", () =>
|
||||||
|
{
|
||||||
|
SelectedMods.Value = new Mod[] { new OsuModClassic() };
|
||||||
|
circle = createCircle(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("Wait until circle is hit", () => circle.Result?.Type == HitResult.Great);
|
||||||
|
AddUntilStep("Wait for miss window", () => Clock.CurrentTime, () => Is.GreaterThanOrEqualTo(circle.HitObject.StartTime + circle.HitObject.HitWindows.WindowFor(HitResult.Miss)));
|
||||||
|
AddAssert("Check circle is still visible", () => circle.Alpha, () => Is.GreaterThan(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHitCircleNoModMiss()
|
||||||
{
|
{
|
||||||
AddStep("Create hit circle", () =>
|
AddStep("Create hit circle", () =>
|
||||||
{
|
{
|
||||||
@ -74,6 +94,16 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddAssert("Opaque when missed", () => alphaAtMiss == 1);
|
AddAssert("Opaque when missed", () => alphaAtMiss == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHitCircleNoModHit()
|
||||||
|
{
|
||||||
|
AddStep("Create hit circle", () =>
|
||||||
|
{
|
||||||
|
SelectedMods.Value = Array.Empty<Mod>();
|
||||||
|
createCircle(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSliderClassicMod()
|
public void TestSliderClassicMod()
|
||||||
{
|
{
|
||||||
@ -100,27 +130,33 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddAssert("Head circle opaque when missed", () => alphaAtMiss == 1);
|
AddAssert("Head circle opaque when missed", () => alphaAtMiss == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createCircle()
|
private TestDrawableHitCircle createCircle(bool shouldHit = false)
|
||||||
{
|
{
|
||||||
alphaAtMiss = null;
|
alphaAtMiss = null;
|
||||||
|
|
||||||
DrawableHitCircle drawableHitCircle = new DrawableHitCircle(new HitCircle
|
TestDrawableHitCircle drawableHitCircle = new TestDrawableHitCircle(new HitCircle
|
||||||
{
|
{
|
||||||
StartTime = Time.Current + 500,
|
StartTime = Time.Current + 500,
|
||||||
Position = new Vector2(250)
|
Position = new Vector2(250),
|
||||||
});
|
}, shouldHit);
|
||||||
|
|
||||||
|
drawableHitCircle.Scale = new Vector2(2f);
|
||||||
|
|
||||||
|
LoadComponent(drawableHitCircle);
|
||||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||||
mod.ApplyToDrawableHitObject(drawableHitCircle);
|
mod.ApplyToDrawableHitObject(drawableHitCircle);
|
||||||
|
|
||||||
drawableHitCircle.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
drawableHitCircle.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
|
|
||||||
drawableHitCircle.OnNewResult += (_, _) =>
|
drawableHitCircle.OnNewResult += (_, result) =>
|
||||||
{
|
{
|
||||||
|
if (!result.IsHit)
|
||||||
alphaAtMiss = drawableHitCircle.Alpha;
|
alphaAtMiss = drawableHitCircle.Alpha;
|
||||||
};
|
};
|
||||||
|
|
||||||
Child = drawableHitCircle;
|
Child = drawableHitCircle;
|
||||||
|
|
||||||
|
return drawableHitCircle;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createSlider()
|
private void createSlider()
|
||||||
@ -138,6 +174,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
drawableSlider.Scale = new Vector2(2f);
|
||||||
|
|
||||||
drawableSlider.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
drawableSlider.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
|
|
||||||
drawableSlider.OnLoadComplete += _ =>
|
drawableSlider.OnLoadComplete += _ =>
|
||||||
@ -145,12 +183,36 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||||
mod.ApplyToDrawableHitObject(drawableSlider.HeadCircle);
|
mod.ApplyToDrawableHitObject(drawableSlider.HeadCircle);
|
||||||
|
|
||||||
drawableSlider.HeadCircle.OnNewResult += (_, _) =>
|
drawableSlider.HeadCircle.OnNewResult += (_, result) =>
|
||||||
{
|
{
|
||||||
|
if (!result.IsHit)
|
||||||
alphaAtMiss = drawableSlider.HeadCircle.Alpha;
|
alphaAtMiss = drawableSlider.HeadCircle.Alpha;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
Child = drawableSlider;
|
Child = drawableSlider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected partial class TestDrawableHitCircle : DrawableHitCircle
|
||||||
|
{
|
||||||
|
private readonly bool shouldHit;
|
||||||
|
|
||||||
|
public TestDrawableHitCircle(HitCircle h, bool shouldHit)
|
||||||
|
: base(h)
|
||||||
|
{
|
||||||
|
this.shouldHit = shouldHit;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
|
{
|
||||||
|
if (shouldHit && !userTriggered && timeOffset >= 0)
|
||||||
|
{
|
||||||
|
// force success
|
||||||
|
ApplyResult(r => r.Type = HitResult.Great);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
base.CheckForResult(userTriggered, timeOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,17 +11,21 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Extensions.TypeExtensions;
|
using osu.Framework.Extensions.TypeExtensions;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Beatmaps.Formats;
|
using osu.Game.Beatmaps.Formats;
|
||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
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.Drawables;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Replays;
|
using osu.Game.Rulesets.Osu.Replays;
|
||||||
using osu.Game.Rulesets.Osu.Scoring;
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
@ -32,7 +36,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
{
|
{
|
||||||
public partial class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene
|
public partial class TestSceneLegacyHitPolicy : RateAdjustedBeatmapTestScene
|
||||||
{
|
{
|
||||||
private readonly OsuHitWindows referenceHitWindows;
|
private readonly OsuHitWindows referenceHitWindows;
|
||||||
|
|
||||||
@ -43,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly string? exportLocation = null;
|
private readonly string? exportLocation = null;
|
||||||
|
|
||||||
public TestSceneObjectOrderedHitPolicy()
|
public TestSceneLegacyHitPolicy()
|
||||||
{
|
{
|
||||||
referenceHitWindows = new OsuHitWindows();
|
referenceHitWindows = new OsuHitWindows();
|
||||||
referenceHitWindows.SetDifficulty(0);
|
referenceHitWindows.SetDifficulty(0);
|
||||||
@ -83,6 +87,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
||||||
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
||||||
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
||||||
|
addClickActionAssert(0, ClickAction.Shake);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -119,6 +124,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
||||||
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
||||||
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
||||||
|
addClickActionAssert(0, ClickAction.Shake);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -155,6 +161,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
||||||
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
||||||
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
||||||
|
addClickActionAssert(0, ClickAction.Shake);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -191,7 +198,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
addJudgementAssert(hitObjects[0], HitResult.Meh);
|
addJudgementAssert(hitObjects[0], HitResult.Meh);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
||||||
addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
|
addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
|
||||||
addJudgementOffsetAssert(hitObjects[0], -90); // time_second_circle - first_circle_time - 90
|
addJudgementOffsetAssert(hitObjects[1], -190); // time_second_circle - first_circle_time - 90
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -229,13 +238,15 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
addJudgementAssert(hitObjects[1], HitResult.Ok);
|
addJudgementAssert(hitObjects[1], HitResult.Ok);
|
||||||
addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
|
addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
|
||||||
addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time
|
addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tests clicking a future circle after a slider's start time, but hitting all slider ticks.
|
/// Tests clicking a future circle after a slider's start time, but hitting the slider head and all slider ticks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
public void TestMissSliderHeadAndHitAllSliderTicks()
|
public void TestHitCircleBeforeSliderHead()
|
||||||
{
|
{
|
||||||
const double time_slider = 1500;
|
const double time_slider = 1500;
|
||||||
const double time_circle = 1510;
|
const double time_circle = 1510;
|
||||||
@ -267,10 +278,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } }
|
new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } }
|
||||||
});
|
});
|
||||||
|
|
||||||
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||||
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
||||||
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -314,6 +327,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||||
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
||||||
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -353,6 +368,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -391,6 +407,291 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||||
|
addClickActionAssert(0, ClickAction.Shake);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
|
addClickActionAssert(2, ClickAction.Hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOverlappingSliders()
|
||||||
|
{
|
||||||
|
const double time_first_slider = 1000;
|
||||||
|
const double time_second_slider = 1200;
|
||||||
|
Vector2 positionFirstSlider = new Vector2(100, 50);
|
||||||
|
Vector2 positionSecondSlider = new Vector2(100, 80);
|
||||||
|
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
||||||
|
|
||||||
|
var hitObjects = new List<OsuHitObject>
|
||||||
|
{
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
StartTime = time_first_slider,
|
||||||
|
Position = positionFirstSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
StartTime = time_second_slider,
|
||||||
|
Position = positionSecondSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton, OsuAction.RightButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint },
|
||||||
|
new OsuReplayFrame { Time = time_second_slider, Position = positionSecondSlider + new Vector2(0, 10), Actions = { OsuAction.LeftButton } },
|
||||||
|
});
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[0], HitResult.Ok);
|
||||||
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestStacksDoNotShake()
|
||||||
|
{
|
||||||
|
const double time_stack_start = 1000;
|
||||||
|
Vector2 position = new Vector2(80);
|
||||||
|
|
||||||
|
var hitObjects = Enumerable.Range(0, 20).Select(i => new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = time_stack_start + i * 100,
|
||||||
|
Position = position
|
||||||
|
}).Cast<OsuHitObject>().ToList();
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_stack_start - 450, Position = new Vector2(55), Actions = { OsuAction.LeftButton } },
|
||||||
|
});
|
||||||
|
|
||||||
|
addClickActionAssert(0, ClickAction.Ignore);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAutopilotReducesHittableRange()
|
||||||
|
{
|
||||||
|
const double time_circle = 1500;
|
||||||
|
Vector2 positionCircle = Vector2.Zero;
|
||||||
|
|
||||||
|
var hitObjects = new List<OsuHitObject>
|
||||||
|
{
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = time_circle,
|
||||||
|
Position = positionCircle
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_circle - 250, Position = positionCircle, Actions = { OsuAction.LeftButton } }
|
||||||
|
}, new Mod[] { new OsuModAutopilot() });
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
||||||
|
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
||||||
|
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
||||||
|
addClickActionAssert(0, ClickAction.Shake);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInputDoesNotFallThroughOverlappingSliders()
|
||||||
|
{
|
||||||
|
const double time_first_slider = 1000;
|
||||||
|
const double time_second_slider = 1250;
|
||||||
|
Vector2 positionFirstSlider = new Vector2(100, 50);
|
||||||
|
Vector2 positionSecondSlider = new Vector2(100, 80);
|
||||||
|
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
||||||
|
|
||||||
|
var hitObjects = new List<OsuHitObject>
|
||||||
|
{
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
StartTime = time_first_slider,
|
||||||
|
Position = positionFirstSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
StartTime = time_second_slider,
|
||||||
|
Position = positionSecondSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint },
|
||||||
|
});
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[0], HitResult.Ok);
|
||||||
|
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
|
||||||
|
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
||||||
|
// the slider head of the first slider prevents the second slider's head from being hit, so the judgement offset should be very late.
|
||||||
|
// this is not strictly done by the hit policy implementation itself (see `OsuModClassic.blockInputToObjectsUnderSliderHead()`),
|
||||||
|
// but we're testing this here anyways to just keep everything related to input handling and note lock in one place.
|
||||||
|
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, referenceHitWindows.WindowFor(HitResult.Meh));
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOverlappingSlidersDontBlockEachOtherWhenFullyJudged()
|
||||||
|
{
|
||||||
|
const double time_first_slider = 1000;
|
||||||
|
const double time_second_slider = 1600;
|
||||||
|
Vector2 positionFirstSlider = new Vector2(100, 50);
|
||||||
|
Vector2 positionSecondSlider = new Vector2(100, 80);
|
||||||
|
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
||||||
|
|
||||||
|
var hitObjects = new List<OsuHitObject>
|
||||||
|
{
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
StartTime = time_first_slider,
|
||||||
|
Position = positionFirstSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
StartTime = time_second_slider,
|
||||||
|
Position = positionSecondSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint },
|
||||||
|
// this frame doesn't do anything on lazer, but is REQUIRED for correct playback on stable,
|
||||||
|
// because stable during replay playback only updates game state _when it encounters a replay frame_
|
||||||
|
new OsuReplayFrame { Time = 1250, Position = midpoint },
|
||||||
|
new OsuReplayFrame { Time = time_second_slider + 50, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
||||||
|
new OsuReplayFrame { Time = time_second_slider + 75, Position = midpoint },
|
||||||
|
});
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[0], HitResult.Ok);
|
||||||
|
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
|
||||||
|
addJudgementAssert(hitObjects[1], HitResult.Ok);
|
||||||
|
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, 50);
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOverlappingHitCirclesDontBlockEachOtherWhenBothVisible()
|
||||||
|
{
|
||||||
|
const double time_first_circle = 1000;
|
||||||
|
const double time_second_circle = 1200;
|
||||||
|
Vector2 positionFirstCircle = new Vector2(100);
|
||||||
|
Vector2 positionSecondCircle = new Vector2(120);
|
||||||
|
var midpoint = (positionFirstCircle + positionSecondCircle) / 2;
|
||||||
|
|
||||||
|
var hitObjects = new List<OsuHitObject>
|
||||||
|
{
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = time_first_circle,
|
||||||
|
Position = positionFirstCircle,
|
||||||
|
},
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = time_second_circle,
|
||||||
|
Position = positionSecondCircle,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_first_circle, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_circle + 25, Position = midpoint },
|
||||||
|
new OsuReplayFrame { Time = time_first_circle + 50, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||||
|
});
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||||
|
addJudgementOffsetAssert(hitObjects[0], 0);
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
||||||
|
addJudgementOffsetAssert(hitObjects[1], -150);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOverlappingHitCirclesDontBlockEachOtherWhenFullyFadedOut()
|
||||||
|
{
|
||||||
|
const double time_first_circle = 1000;
|
||||||
|
const double time_second_circle = 1200;
|
||||||
|
const double time_third_circle = 1400;
|
||||||
|
Vector2 positionFirstCircle = new Vector2(100);
|
||||||
|
Vector2 positionSecondCircle = new Vector2(200);
|
||||||
|
|
||||||
|
var hitObjects = new List<OsuHitObject>
|
||||||
|
{
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = time_first_circle,
|
||||||
|
Position = positionFirstCircle,
|
||||||
|
},
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = time_second_circle,
|
||||||
|
Position = positionSecondCircle,
|
||||||
|
},
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = time_third_circle,
|
||||||
|
Position = positionFirstCircle,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_first_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_circle + 50, Position = positionFirstCircle },
|
||||||
|
new OsuReplayFrame { Time = time_second_circle - 50, Position = positionSecondCircle },
|
||||||
|
new OsuReplayFrame { Time = time_second_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } },
|
||||||
|
new OsuReplayFrame { Time = time_second_circle + 50, Position = positionSecondCircle },
|
||||||
|
new OsuReplayFrame { Time = time_third_circle - 50, Position = positionFirstCircle },
|
||||||
|
new OsuReplayFrame { Time = time_third_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
|
||||||
|
new OsuReplayFrame { Time = time_third_circle + 50, Position = positionFirstCircle },
|
||||||
|
});
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||||
|
addJudgementOffsetAssert(hitObjects[0], 0);
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||||
|
addJudgementOffsetAssert(hitObjects[1], 0);
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[2], HitResult.Great);
|
||||||
|
addJudgementOffsetAssert(hitObjects[2], 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
||||||
@ -408,17 +709,36 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
|
private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
|
||||||
{
|
{
|
||||||
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
|
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
|
||||||
() => judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, () => Is.EqualTo(offset).Within(100));
|
() => judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addJudgementOffsetAssert(string name, Func<OsuHitObject?> hitObject, double offset)
|
||||||
|
{
|
||||||
|
AddAssert($"{name} @ judged at {offset}",
|
||||||
|
() => judgementResults.Single(r => r.HitObject == hitObject()).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addClickActionAssert(int inputIndex, ClickAction action)
|
||||||
|
=> AddAssert($"input #{inputIndex} resulted in {action}", () => testPolicy.ClickActions[inputIndex], () => Is.EqualTo(action));
|
||||||
|
|
||||||
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
||||||
private List<JudgementResult> judgementResults = null!;
|
private List<JudgementResult> judgementResults = null!;
|
||||||
|
private TestLegacyHitPolicy testPolicy = null!;
|
||||||
|
|
||||||
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames, [CallerMemberName] string testCaseName = "")
|
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames, IEnumerable<Mod>? extraMods = null, [CallerMemberName] string testCaseName = "")
|
||||||
{
|
{
|
||||||
|
List<Mod> mods = null!;
|
||||||
IBeatmap playableBeatmap = null!;
|
IBeatmap playableBeatmap = null!;
|
||||||
Score score = null!;
|
Score score = null!;
|
||||||
|
|
||||||
|
AddStep("set up mods", () =>
|
||||||
|
{
|
||||||
|
mods = new List<Mod> { new OsuModClassic() };
|
||||||
|
|
||||||
|
if (extraMods != null)
|
||||||
|
mods.AddRange(extraMods);
|
||||||
|
});
|
||||||
|
|
||||||
AddStep("create beatmap", () =>
|
AddStep("create beatmap", () =>
|
||||||
{
|
{
|
||||||
var cpi = new ControlPointInfo();
|
var cpi = new ControlPointInfo();
|
||||||
@ -461,7 +781,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
ScoreInfo =
|
ScoreInfo =
|
||||||
{
|
{
|
||||||
Ruleset = new OsuRuleset().RulesetInfo,
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
BeatmapInfo = playableBeatmap.BeatmapInfo
|
BeatmapInfo = playableBeatmap.BeatmapInfo,
|
||||||
|
Mods = mods.ToArray()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -495,7 +816,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
AddStep("load player", () =>
|
AddStep("load player", () =>
|
||||||
{
|
{
|
||||||
SelectedMods.Value = new[] { new OsuModClassic() };
|
SelectedMods.Value = mods.ToArray();
|
||||||
|
|
||||||
var p = new ScoreAccessibleReplayPlayer(score);
|
var p = new ScoreAccessibleReplayPlayer(score);
|
||||||
|
|
||||||
@ -513,6 +834,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||||
|
AddStep("Substitute hit policy", () =>
|
||||||
|
{
|
||||||
|
var playfield = currentPlayer.ChildrenOfType<OsuPlayfield>().Single();
|
||||||
|
var currentPolicy = playfield.HitPolicy;
|
||||||
|
playfield.HitPolicy = testPolicy = new TestLegacyHitPolicy(currentPolicy);
|
||||||
|
});
|
||||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -540,5 +867,24 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class TestLegacyHitPolicy : LegacyHitPolicy
|
||||||
|
{
|
||||||
|
private readonly IHitPolicy currentPolicy;
|
||||||
|
|
||||||
|
public TestLegacyHitPolicy(IHitPolicy currentPolicy)
|
||||||
|
{
|
||||||
|
this.currentPolicy = currentPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ClickAction> ClickActions { get; } = new List<ClickAction>();
|
||||||
|
|
||||||
|
public override ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result)
|
||||||
|
{
|
||||||
|
var action = currentPolicy.CheckHittable(hitObject, time, result);
|
||||||
|
ClickActions.Add(action);
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
176
osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs
Normal file
176
osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
// 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.Judgements;
|
||||||
|
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Tests.Visual.Gameplay;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public partial class TestSceneScoring : ScoringTestScene
|
||||||
|
{
|
||||||
|
private Bindable<double> scoreMultiplier { get; } = new BindableDouble
|
||||||
|
{
|
||||||
|
Default = 4,
|
||||||
|
Value = 4
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override IBeatmap CreateBeatmap(int maxCombo)
|
||||||
|
{
|
||||||
|
var beatmap = new OsuBeatmap();
|
||||||
|
for (int i = 0; i < maxCombo; i++)
|
||||||
|
beatmap.HitObjects.Add(new HitCircle());
|
||||||
|
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 OsuProcessorBasedScoringAlgorithm(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 });
|
||||||
|
});
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int base_great = 300;
|
||||||
|
private const int base_ok = 100;
|
||||||
|
|
||||||
|
private class ScoreV1 : IScoringAlgorithm
|
||||||
|
{
|
||||||
|
private int currentCombo;
|
||||||
|
|
||||||
|
public BindableDouble ScoreMultiplier { get; } = new BindableDouble();
|
||||||
|
|
||||||
|
public void ApplyHit() => applyHitV1(base_great);
|
||||||
|
public void ApplyNonPerfect() => applyHitV1(base_ok);
|
||||||
|
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 double currentBaseScore;
|
||||||
|
private double maxBaseScore;
|
||||||
|
private int currentHits;
|
||||||
|
|
||||||
|
private readonly double comboPortionMax;
|
||||||
|
private readonly int maxCombo;
|
||||||
|
|
||||||
|
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(base_great);
|
||||||
|
public void ApplyNonPerfect() => applyHitV2(base_ok);
|
||||||
|
|
||||||
|
private void applyHitV2(int baseScore)
|
||||||
|
{
|
||||||
|
maxBaseScore += base_great;
|
||||||
|
currentBaseScore += baseScore;
|
||||||
|
comboPortion += baseScore * (1 + ++currentCombo / 10.0);
|
||||||
|
|
||||||
|
currentHits++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyMiss()
|
||||||
|
{
|
||||||
|
currentHits++;
|
||||||
|
maxBaseScore += base_great;
|
||||||
|
currentCombo = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long TotalScore
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
double accuracy = currentBaseScore / maxBaseScore;
|
||||||
|
|
||||||
|
return (int)Math.Round
|
||||||
|
(
|
||||||
|
700000 * comboPortion / comboPortionMax +
|
||||||
|
300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OsuProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm
|
||||||
|
{
|
||||||
|
public OsuProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode)
|
||||||
|
: base(beatmap, mode)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor();
|
||||||
|
protected override JudgementResult CreatePerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great };
|
||||||
|
protected override JudgementResult CreateNonPerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok };
|
||||||
|
protected override JudgementResult CreateMissJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
StartTime = time_slider_start,
|
StartTime = time_slider_start,
|
||||||
Position = new Vector2(0, 0),
|
Position = new Vector2(0, 0),
|
||||||
SliderVelocity = velocity,
|
SliderVelocityMultiplier = velocity,
|
||||||
Path = new SliderPath(PathType.Linear, new[]
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
{
|
{
|
||||||
Vector2.Zero,
|
Vector2.Zero,
|
||||||
|
@ -349,7 +349,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
StartTime = time_slider_start,
|
StartTime = time_slider_start,
|
||||||
Position = new Vector2(0, 0),
|
Position = new Vector2(0, 0),
|
||||||
SliderVelocity = 0.1f,
|
SliderVelocityMultiplier = 0.1f,
|
||||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||||
{
|
{
|
||||||
Vector2.Zero,
|
Vector2.Zero,
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -336,6 +336,52 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
addJudgementAssert(hitObjects[1], HitResult.IgnoreHit);
|
addJudgementAssert(hitObjects[1], HitResult.IgnoreHit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInputFallsThroughJudgedSliders()
|
||||||
|
{
|
||||||
|
const double time_first_slider = 1000;
|
||||||
|
const double time_second_slider = 1250;
|
||||||
|
Vector2 positionFirstSlider = new Vector2(100, 50);
|
||||||
|
Vector2 positionSecondSlider = new Vector2(100, 80);
|
||||||
|
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
||||||
|
|
||||||
|
var hitObjects = new List<OsuHitObject>
|
||||||
|
{
|
||||||
|
new TestSlider
|
||||||
|
{
|
||||||
|
StartTime = time_first_slider,
|
||||||
|
Position = positionFirstSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
new TestSlider
|
||||||
|
{
|
||||||
|
StartTime = time_second_slider,
|
||||||
|
Position = positionSecondSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint },
|
||||||
|
});
|
||||||
|
|
||||||
|
addJudgementAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, HitResult.Great);
|
||||||
|
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
|
||||||
|
addJudgementAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great);
|
||||||
|
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, -200);
|
||||||
|
}
|
||||||
|
|
||||||
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
||||||
{
|
{
|
||||||
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
|
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
|
||||||
@ -354,6 +400,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
() => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
|
() => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addJudgementOffsetAssert(string name, Func<OsuHitObject> hitObject, double offset)
|
||||||
|
{
|
||||||
|
AddAssert($"{name} @ judged at {offset}",
|
||||||
|
() => judgementResults.Single(r => r.HitObject == hitObject()).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
||||||
|
}
|
||||||
|
|
||||||
private ScoreAccessibleReplayPlayer currentPlayer;
|
private ScoreAccessibleReplayPlayer currentPlayer;
|
||||||
private List<JudgementResult> judgementResults;
|
private List<JudgementResult> judgementResults;
|
||||||
|
|
||||||
@ -399,7 +451,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
public TestSlider()
|
public TestSlider()
|
||||||
{
|
{
|
||||||
SliderVelocity = 0.1f;
|
SliderVelocityMultiplier = 0.1f;
|
||||||
|
|
||||||
DefaultsApplied += _ =>
|
DefaultsApplied += _ =>
|
||||||
{
|
{
|
||||||
|
@ -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>
|
||||||
|
@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
// 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,
|
||||||
GenerateTicks = generateTicksData?.GenerateTicks ?? true,
|
GenerateTicks = generateTicksData?.GenerateTicks ?? true,
|
||||||
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1,
|
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1,
|
||||||
}.Yield();
|
}.Yield();
|
||||||
|
|
||||||
case IHasDuration endTimeData:
|
case IHasDuration endTimeData:
|
||||||
|
@ -85,9 +85,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
BeginPlacement();
|
BeginPlacement();
|
||||||
|
|
||||||
double? nearestSliderVelocity = (editorBeatmap.HitObjects
|
double? nearestSliderVelocity = (editorBeatmap.HitObjects
|
||||||
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocity;
|
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocityMultiplier;
|
||||||
|
|
||||||
HitObject.SliderVelocity = nearestSliderVelocity ?? 1;
|
HitObject.SliderVelocityMultiplier = nearestSliderVelocity ?? 1;
|
||||||
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
|
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
|
||||||
|
|
||||||
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.
|
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.
|
||||||
|
@ -11,6 +11,7 @@ using osu.Game.Rulesets.Objects;
|
|||||||
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 osu.Game.Rulesets.Osu.Scoring;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
@ -24,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);
|
||||||
|
|
||||||
@ -57,7 +55,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
var osuRuleset = (DrawableOsuRuleset)drawableRuleset;
|
var osuRuleset = (DrawableOsuRuleset)drawableRuleset;
|
||||||
|
|
||||||
if (ClassicNoteLock.Value)
|
if (ClassicNoteLock.Value)
|
||||||
osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy();
|
{
|
||||||
|
double hittableRange = OsuHitWindows.MISS_WINDOW - (drawableRuleset.Mods.OfType<OsuModAutopilot>().Any() ? 200 : 0);
|
||||||
|
osuRuleset.Playfield.HitPolicy = new LegacyHitPolicy(hittableRange);
|
||||||
|
}
|
||||||
|
|
||||||
usingHiddenFading = drawableRuleset.Mods.OfType<OsuModHidden>().SingleOrDefault()?.OnlyFadeApproachCircles.Value == false;
|
usingHiddenFading = drawableRuleset.Mods.OfType<OsuModHidden>().SingleOrDefault()?.OnlyFadeApproachCircles.Value == false;
|
||||||
}
|
}
|
||||||
@ -67,9 +68,12 @@ 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);
|
||||||
|
|
||||||
|
if (ClassicNoteLock.Value)
|
||||||
|
blockInputToObjectsUnderSliderHead(head);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DrawableSliderTail tail:
|
case DrawableSliderTail tail:
|
||||||
@ -79,19 +83,39 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
case DrawableHitCircle circle:
|
case DrawableHitCircle circle:
|
||||||
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
||||||
applyEarlyFading(circle);
|
applyEarlyFading(circle);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// On stable, slider heads that have already been hit block input from reaching objects that may be underneath them
|
||||||
|
/// until the sliders they're part of have been fully judged.
|
||||||
|
/// The purpose of this method is to restore that behaviour.
|
||||||
|
/// In order to avoid introducing yet another confusing config option, this behaviour is roped into the general notion of "note lock".
|
||||||
|
/// </summary>
|
||||||
|
private static void blockInputToObjectsUnderSliderHead(DrawableSliderHead slider)
|
||||||
|
{
|
||||||
|
var oldHitAction = slider.HitArea.Hit;
|
||||||
|
slider.HitArea.Hit = () =>
|
||||||
|
{
|
||||||
|
oldHitAction?.Invoke();
|
||||||
|
return !slider.DrawableSlider.AllJudged;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private void applyEarlyFading(DrawableHitCircle circle)
|
private void applyEarlyFading(DrawableHitCircle circle)
|
||||||
{
|
{
|
||||||
circle.ApplyCustomUpdateState += (o, _) =>
|
circle.ApplyCustomUpdateState += (dho, state) =>
|
||||||
{
|
{
|
||||||
using (o.BeginAbsoluteSequence(o.StateUpdateTime))
|
using (dho.BeginAbsoluteSequence(dho.StateUpdateTime))
|
||||||
{
|
{
|
||||||
double okWindow = o.HitObject.HitWindows.WindowFor(HitResult.Ok);
|
if (state != ArmedState.Hit)
|
||||||
double lateMissFadeTime = o.HitObject.HitWindows.WindowFor(HitResult.Meh) - okWindow;
|
{
|
||||||
o.Delay(okWindow).FadeOut(lateMissFadeTime);
|
double okWindow = dho.HitObject.HitWindows.WindowFor(HitResult.Ok);
|
||||||
|
double lateMissFadeTime = dho.HitObject.HitWindows.WindowFor(HitResult.Meh) - okWindow;
|
||||||
|
dho.Delay(okWindow).FadeOut(lateMissFadeTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,19 @@
|
|||||||
// 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.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Mods
|
namespace osu.Game.Rulesets.Osu.Mods
|
||||||
{
|
{
|
||||||
public class OsuModDifficultyAdjust : ModDifficultyAdjust
|
public partial class OsuModDifficultyAdjust : ModDifficultyAdjust
|
||||||
{
|
{
|
||||||
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
||||||
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
|
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
|
||||||
@ -20,12 +26,13 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
ReadCurrentFromDifficulty = diff => diff.CircleSize,
|
ReadCurrentFromDifficulty = diff => diff.CircleSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(ApproachRateSettingsControl))]
|
||||||
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
|
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
|
||||||
{
|
{
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
|
ExtendedMinValue = -10,
|
||||||
ExtendedMaxValue = 11,
|
ExtendedMaxValue = 11,
|
||||||
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
|
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
|
||||||
};
|
};
|
||||||
@ -53,5 +60,34 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value;
|
if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value;
|
||||||
if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value;
|
if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private partial class ApproachRateSettingsControl : DifficultyAdjustSettingsControl
|
||||||
|
{
|
||||||
|
protected override RoundedSliderBar<float> CreateSlider(BindableNumber<float> current) =>
|
||||||
|
new ApproachRateSlider
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Current = current,
|
||||||
|
KeyboardStep = 0.1f,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A slider bar with more detailed approach rate info for its given value
|
||||||
|
/// </summary>
|
||||||
|
public partial class ApproachRateSlider : RoundedSliderBar<float>
|
||||||
|
{
|
||||||
|
public override LocalisableString TooltipText =>
|
||||||
|
(Current as BindableNumber<float>)?.MinValue < 0
|
||||||
|
? $"{base.TooltipText} ({getPreemptTime(Current.Value):0} ms)"
|
||||||
|
: base.TooltipText;
|
||||||
|
|
||||||
|
private double getPreemptTime(float approachRate)
|
||||||
|
{
|
||||||
|
var hitCircle = new HitCircle();
|
||||||
|
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { ApproachRate = approachRate });
|
||||||
|
return hitCircle.TimePreempt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,8 +68,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
public void OnSliderTrackingChange(ValueChangedEvent<bool> e)
|
public void OnSliderTrackingChange(ValueChangedEvent<bool> e)
|
||||||
{
|
{
|
||||||
// If a slider is in a tracking state, a further dim should be applied to the (remaining) visible portion of the playfield over a brief duration.
|
// If a slider is in a tracking state, a further dim should be applied to the (remaining) visible portion of the playfield.
|
||||||
this.TransformTo(nameof(FlashlightDim), e.NewValue ? 0.8f : 0.0f, 50);
|
FlashlightDim = e.NewValue ? 0.8f : 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||||
|
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
ComboOffset = original.ComboOffset;
|
ComboOffset = original.ComboOffset;
|
||||||
LegacyLastTickOffset = original.LegacyLastTickOffset;
|
LegacyLastTickOffset = original.LegacyLastTickOffset;
|
||||||
TickDistanceMultiplier = original.TickDistanceMultiplier;
|
TickDistanceMultiplier = original.TickDistanceMultiplier;
|
||||||
SliderVelocity = original.SliderVelocity;
|
SliderVelocityMultiplier = original.SliderVelocityMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
|
@ -18,6 +18,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
|||||||
using osu.Game.Rulesets.Osu.Judgements;
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
using osu.Game.Rulesets.Osu.Skinning;
|
using osu.Game.Rulesets.Osu.Skinning;
|
||||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -154,12 +155,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
}
|
}
|
||||||
|
|
||||||
var result = ResultFor(timeOffset);
|
var result = ResultFor(timeOffset);
|
||||||
|
var clickAction = CheckHittable?.Invoke(this, Time.Current, result);
|
||||||
|
|
||||||
if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
|
if (clickAction == ClickAction.Shake)
|
||||||
{
|
|
||||||
Shake();
|
Shake();
|
||||||
|
|
||||||
|
if (result == HitResult.None || clickAction != ClickAction.Hit)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
ApplyResult(r =>
|
ApplyResult(r =>
|
||||||
{
|
{
|
||||||
@ -259,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
case OsuAction.RightButton:
|
case OsuAction.RightButton:
|
||||||
if (IsHovered && (Hit?.Invoke() ?? false))
|
if (IsHovered && (Hit?.Invoke() ?? false))
|
||||||
{
|
{
|
||||||
HitAction = e.Action;
|
HitAction ??= e.Action;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,8 @@ using osu.Game.Rulesets.Judgements;
|
|||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Judgements;
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
using osu.Game.Rulesets.Osu.Scoring;
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -30,10 +32,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
protected override float SamplePlaybackPosition => CalculateDrawableRelativePosition(this);
|
protected override float SamplePlaybackPosition => CalculateDrawableRelativePosition(this);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this <see cref="DrawableOsuHitObject"/> can be hit, given a time value.
|
/// What action this <see cref="DrawableOsuHitObject"/> should take in response to a
|
||||||
/// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false.
|
/// click at the given time value.
|
||||||
|
/// If non-null, judgements will be ignored for return values of <see cref="ClickAction.Ignore"/>
|
||||||
|
/// and <see cref="ClickAction.Shake"/>, and this hit object will be shaken for return values of
|
||||||
|
/// <see cref="ClickAction.Shake"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<DrawableHitObject, double, bool> CheckHittable;
|
public Func<DrawableHitObject, double, HitResult, ClickAction> CheckHittable;
|
||||||
|
|
||||||
protected DrawableOsuHitObject(OsuHitObject hitObject)
|
protected DrawableOsuHitObject(OsuHitObject hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
|
@ -228,7 +228,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)
|
||||||
{
|
{
|
||||||
@ -317,7 +317,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;
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,10 @@
|
|||||||
|
|
||||||
#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.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
@ -23,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;
|
||||||
@ -60,24 +53,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
pathVersion.BindTo(DrawableSlider.PathVersion);
|
pathVersion.BindTo(DrawableSlider.PathVersion);
|
||||||
|
|
||||||
CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? true;
|
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)
|
||||||
|
@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
|
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
|
||||||
|
|
||||||
private int wholeSpins;
|
private int completedFullSpins;
|
||||||
|
|
||||||
private void updateBonusScore()
|
private void updateBonusScore()
|
||||||
{
|
{
|
||||||
@ -295,14 +295,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
int spins = (int)(Result.RateAdjustedRotation / 360);
|
int spins = (int)(Result.RateAdjustedRotation / 360);
|
||||||
|
|
||||||
if (spins < wholeSpins)
|
if (spins < completedFullSpins)
|
||||||
{
|
{
|
||||||
// rewinding, silently handle
|
// rewinding, silently handle
|
||||||
wholeSpins = spins;
|
completedFullSpins = spins;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (wholeSpins != spins)
|
while (completedFullSpins != spins)
|
||||||
{
|
{
|
||||||
var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
|
var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
|
||||||
|
|
||||||
@ -312,10 +312,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
tick.TriggerResult(true);
|
tick.TriggerResult(true);
|
||||||
|
|
||||||
if (tick is DrawableSpinnerBonusTick)
|
if (tick is DrawableSpinnerBonusTick)
|
||||||
gainedBonus.Value = score_per_tick * (spins - HitObject.SpinsRequired);
|
gainedBonus.Value = score_per_tick * (spins - HitObject.SpinsRequiredForBonus);
|
||||||
}
|
}
|
||||||
|
|
||||||
wholeSpins++;
|
completedFullSpins++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ using osu.Game.Audio;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
using osu.Game.Rulesets.Osu.Judgements;
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
@ -113,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
public double SpanDuration => Duration / this.SpanCount();
|
public double SpanDuration => Duration / this.SpanCount();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Velocity of this <see cref="Slider"/>.
|
/// The computed velocity of this <see cref="Slider"/>. This is the amount of path distance travelled in 1 ms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double Velocity { get; private set; }
|
public double Velocity { get; private set; }
|
||||||
|
|
||||||
@ -134,17 +135,16 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool OnlyJudgeNestedObjects = true;
|
public bool OnlyJudgeNestedObjects = true;
|
||||||
|
|
||||||
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
|
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
|
||||||
{
|
{
|
||||||
Precision = 0.01,
|
|
||||||
MinValue = 0.1,
|
MinValue = 0.1,
|
||||||
MaxValue = 10
|
MaxValue = 10
|
||||||
};
|
};
|
||||||
|
|
||||||
public double SliderVelocity
|
public double SliderVelocityMultiplier
|
||||||
{
|
{
|
||||||
get => SliderVelocityBindable.Value;
|
get => SliderVelocityMultiplierBindable.Value;
|
||||||
set => SliderVelocityBindable.Value = value;
|
set => SliderVelocityMultiplierBindable.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool GenerateTicks { get; set; } = true;
|
public bool GenerateTicks { get; set; } = true;
|
||||||
@ -167,9 +167,11 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
|
|
||||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||||
|
|
||||||
double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * SliderVelocity;
|
Velocity = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier / LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(this, timingPoint, OsuRuleset.SHORT_NAME);
|
||||||
|
// WARNING: this is intentionally not computed as `BASE_SCORING_DISTANCE * difficulty.SliderMultiplier`
|
||||||
|
// for backwards compatibility reasons (intentionally introducing floating point errors to match stable).
|
||||||
|
double scoringDistance = Velocity * timingPoint.BeatLength;
|
||||||
|
|
||||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
|
||||||
TickDistance = GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity;
|
TickDistance = GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,16 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int SpinsRequired { get; protected set; } = 1;
|
public int SpinsRequired { get; protected set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of spins required to start receiving bonus score. The first bonus is awarded on this spin count.
|
||||||
|
/// </summary>
|
||||||
|
public int SpinsRequiredForBonus => SpinsRequired + bonus_spins_gap;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The gap between spinner completion and the first bonus-awarding spin.
|
||||||
|
/// </summary>
|
||||||
|
private const int bonus_spins_gap = 2;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of spins available to give bonus, beyond <see cref="SpinsRequired"/>.
|
/// Number of spins available to give bonus, beyond <see cref="SpinsRequired"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -42,25 +52,20 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||||
|
|
||||||
// spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being.
|
const double maximum_rotations_per_second = 477f / 60f;
|
||||||
const double stable_matching_fudge = 0.6;
|
|
||||||
|
|
||||||
// close to 477rpm
|
|
||||||
const double maximum_rotations_per_second = 8;
|
|
||||||
|
|
||||||
double secondsDuration = Duration / 1000;
|
double secondsDuration = Duration / 1000;
|
||||||
|
double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 1.5, 2.5, 3.75);
|
||||||
double minimumRotationsPerSecond = stable_matching_fudge * IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
|
|
||||||
|
|
||||||
SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond);
|
SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond);
|
||||||
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration);
|
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration) - bonus_spins_gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
base.CreateNestedHitObjects(cancellationToken);
|
base.CreateNestedHitObjects(cancellationToken);
|
||||||
|
|
||||||
int totalSpins = MaximumBonusSpins + SpinsRequired;
|
int totalSpins = MaximumBonusSpins + SpinsRequired + bonus_spins_gap;
|
||||||
|
|
||||||
for (int i = 0; i < totalSpins; i++)
|
for (int i = 0; i < totalSpins; i++)
|
||||||
{
|
{
|
||||||
@ -68,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
|
|
||||||
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
|
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
|
||||||
|
|
||||||
AddNested(i < SpinsRequired
|
AddNested(i < SpinsRequiredForBonus
|
||||||
? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration }
|
? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration }
|
||||||
: new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration, Samples = new[] { CreateHitSampleInfo("spinnerbonus") } });
|
: new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration, Samples = new[] { CreateHitSampleInfo("spinnerbonus") } });
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"Mappings": [
|
||||||
|
{
|
||||||
|
"StartTime": 7493.0,
|
||||||
|
"Objects": [
|
||||||
|
{
|
||||||
|
"StartTime": 7493.0,
|
||||||
|
"EndTime": 7493.0,
|
||||||
|
"X": 130.0,
|
||||||
|
"Y": 232.0,
|
||||||
|
"StackOffset": {
|
||||||
|
"X": 0.0,
|
||||||
|
"Y": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"StartTime": 7817.0,
|
||||||
|
"EndTime": 7817.0,
|
||||||
|
"X": 30.9946651,
|
||||||
|
"Y": 208.5157,
|
||||||
|
"StackOffset": {
|
||||||
|
"X": 0.0,
|
||||||
|
"Y": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"StartTime": 7843.0,
|
||||||
|
"EndTime": 7843.0,
|
||||||
|
"X": 33.7820168,
|
||||||
|
"Y": 208.9957,
|
||||||
|
"StackOffset": {
|
||||||
|
"X": 0.0,
|
||||||
|
"Y": 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
osu file format v14
|
||||||
|
|
||||||
|
[General]
|
||||||
|
StackLeniency: 0.7
|
||||||
|
|
||||||
|
[Difficulty]
|
||||||
|
HPDrainRate:3
|
||||||
|
CircleSize:3.4
|
||||||
|
OverallDifficulty:4
|
||||||
|
ApproachRate:5.5
|
||||||
|
SliderMultiplier:1.1
|
||||||
|
SliderTickRate:1
|
||||||
|
|
||||||
|
[Events]
|
||||||
|
//Background and Video events
|
||||||
|
0,0,"aa.jpg",0,0
|
||||||
|
//Break Periods
|
||||||
|
//Storyboard Layer 0 (Background)
|
||||||
|
//Storyboard Layer 1 (Fail)
|
||||||
|
//Storyboard Layer 2 (Pass)
|
||||||
|
//Storyboard Layer 3 (Foreground)
|
||||||
|
//Storyboard Layer 4 (Overlay)
|
||||||
|
//Storyboard Sound Samples
|
||||||
|
|
||||||
|
[TimingPoints]
|
||||||
|
6967,350.877192982456,6,2,1,55,1,0
|
||||||
|
6967,-100,3,2,1,55,0,0
|
||||||
|
7493,-111.111111111111,3,2,1,55,0,0
|
||||||
|
|
||||||
|
[HitObjects]
|
||||||
|
130,232,7493,6,0,P|78:218|28:208,1,101.82149697876,0|0,3:2|2:0,2:0:0:0:
|
@ -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,9 +1,8 @@
|
|||||||
// 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 osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
@ -13,9 +12,9 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class AnyOrderHitPolicy : IHitPolicy
|
public class AnyOrderHitPolicy : IHitPolicy
|
||||||
{
|
{
|
||||||
public IHitObjectContainer HitObjectContainer { get; set; }
|
public IHitObjectContainer HitObjectContainer { get; set; } = null!;
|
||||||
|
|
||||||
public bool IsHittable(DrawableHitObject hitObject, double time) => true;
|
public ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result) => ClickAction.Hit;
|
||||||
|
|
||||||
public void HandleHit(DrawableHitObject hitObject)
|
public void HandleHit(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
|
18
osu.Game.Rulesets.Osu/UI/ClickAction.cs
Normal file
18
osu.Game.Rulesets.Osu/UI/ClickAction.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An action that an <see cref="IHitPolicy"/> recommends be taken in response to a click
|
||||||
|
/// on a <see cref="DrawableOsuHitObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
public enum ClickAction
|
||||||
|
{
|
||||||
|
Ignore,
|
||||||
|
Shake,
|
||||||
|
Hit
|
||||||
|
}
|
||||||
|
}
|
@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
if (time - part.Time >= 1)
|
if (time - part.Time >= 1)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
|
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
|
||||||
TexturePosition = textureRect.BottomLeft,
|
TexturePosition = textureRect.BottomLeft,
|
||||||
@ -295,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
Time = part.Time
|
Time = part.Time
|
||||||
});
|
});
|
||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
|
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
|
||||||
TexturePosition = textureRect.BottomRight,
|
TexturePosition = textureRect.BottomRight,
|
||||||
@ -304,7 +304,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
Time = part.Time
|
Time = part.Time
|
||||||
});
|
});
|
||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
|
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
|
||||||
TexturePosition = textureRect.TopRight,
|
TexturePosition = textureRect.TopRight,
|
||||||
@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
Time = part.Time
|
Time = part.Time
|
||||||
});
|
});
|
||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
|
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
|
||||||
TexturePosition = textureRect.TopLeft,
|
TexturePosition = textureRect.TopLeft,
|
||||||
@ -362,22 +362,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
[VertexMember(1, VertexAttribPointerType.Float)]
|
[VertexMember(1, VertexAttribPointerType.Float)]
|
||||||
public float Time;
|
public float Time;
|
||||||
|
|
||||||
[VertexMember(1, VertexAttribPointerType.Int)]
|
|
||||||
private readonly int maskingIndex;
|
|
||||||
|
|
||||||
public TexturedTrailVertex(IRenderer renderer)
|
|
||||||
{
|
|
||||||
this = default;
|
|
||||||
maskingIndex = renderer.CurrentMaskingIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(TexturedTrailVertex other)
|
public bool Equals(TexturedTrailVertex other)
|
||||||
{
|
{
|
||||||
return Position.Equals(other.Position)
|
return Position.Equals(other.Position)
|
||||||
&& TexturePosition.Equals(other.TexturePosition)
|
&& TexturePosition.Equals(other.TexturePosition)
|
||||||
&& Colour.Equals(other.Colour)
|
&& Colour.Equals(other.Colour)
|
||||||
&& Time.Equals(other.Time)
|
&& Time.Equals(other.Time);
|
||||||
&& maskingIndex == other.maskingIndex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
@ -19,8 +20,9 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to check.</param>
|
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to check.</param>
|
||||||
/// <param name="time">The time to check.</param>
|
/// <param name="time">The time to check.</param>
|
||||||
|
/// <param name="result">The result that the object would be judged with if hit.</param>
|
||||||
/// <returns>Whether <paramref name="hitObject"/> can be hit at the given <paramref name="time"/>.</returns>
|
/// <returns>Whether <paramref name="hitObject"/> can be hit at the given <paramref name="time"/>.</returns>
|
||||||
bool IsHittable(DrawableHitObject hitObject, double time);
|
ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles a <see cref="HitObject"/> being hit.
|
/// Handles a <see cref="HitObject"/> being hit.
|
||||||
|
72
osu.Game.Rulesets.Osu/UI/LegacyHitPolicy.cs
Normal file
72
osu.Game.Rulesets.Osu/UI/LegacyHitPolicy.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures that <see cref="HitObject"/>s are hit in order of appearance. The classic note lock.
|
||||||
|
/// <remarks>
|
||||||
|
/// Hits will be blocked until the previous <see cref="HitObject"/>s have been judged.
|
||||||
|
/// </remarks>
|
||||||
|
/// </summary>
|
||||||
|
public class LegacyHitPolicy : IHitPolicy
|
||||||
|
{
|
||||||
|
public IHitObjectContainer? HitObjectContainer { get; set; }
|
||||||
|
|
||||||
|
private readonly double hittableRange;
|
||||||
|
|
||||||
|
public LegacyHitPolicy(double hittableRange = OsuHitWindows.MISS_WINDOW)
|
||||||
|
{
|
||||||
|
this.hittableRange = hittableRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HandleHit(DrawableHitObject hitObject)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result)
|
||||||
|
{
|
||||||
|
if (HitObjectContainer == null)
|
||||||
|
throw new InvalidOperationException($"{nameof(HitObjectContainer)} should be set before {nameof(CheckHittable)} is called.");
|
||||||
|
|
||||||
|
var aliveObjects = HitObjectContainer.AliveObjects.ToList();
|
||||||
|
int index = aliveObjects.IndexOf(hitObject);
|
||||||
|
|
||||||
|
if (index > 0)
|
||||||
|
{
|
||||||
|
var previousHitObject = (DrawableOsuHitObject)aliveObjects[index - 1];
|
||||||
|
if (previousHitObject.HitObject.StackHeight > 0 && !previousHitObject.AllJudged)
|
||||||
|
return ClickAction.Ignore;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == HitResult.None)
|
||||||
|
return ClickAction.Shake;
|
||||||
|
|
||||||
|
foreach (DrawableHitObject testObject in aliveObjects)
|
||||||
|
{
|
||||||
|
if (testObject.AllJudged)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// if we found the object being checked, we can move on to the final timing test.
|
||||||
|
if (testObject == hitObject)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// for all other objects, we check for validity and block the hit if any are still valid.
|
||||||
|
// 3ms of extra leniency to account for slightly unsnapped objects.
|
||||||
|
if (testObject.HitObject.GetEndTime() + 3 < hitObject.HitObject.StartTime)
|
||||||
|
return ClickAction.Shake;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.Abs(hitObject.HitObject.StartTime - time) < hittableRange ? ClickAction.Hit : ClickAction.Shake;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,56 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Game.Rulesets.Objects;
|
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
|
||||||
using osu.Game.Rulesets.UI;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Ensures that <see cref="HitObject"/>s are hit in order of appearance. The classic note lock.
|
|
||||||
/// <remarks>
|
|
||||||
/// Hits will be blocked until the previous <see cref="HitObject"/>s have been judged.
|
|
||||||
/// </remarks>
|
|
||||||
/// </summary>
|
|
||||||
public class ObjectOrderedHitPolicy : IHitPolicy
|
|
||||||
{
|
|
||||||
public IHitObjectContainer HitObjectContainer { get; set; }
|
|
||||||
|
|
||||||
public bool IsHittable(DrawableHitObject hitObject, double time) => enumerateHitObjectsUpTo(hitObject.HitObject.StartTime).All(obj => obj.AllJudged);
|
|
||||||
|
|
||||||
public void HandleHit(DrawableHitObject hitObject)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
|
|
||||||
{
|
|
||||||
foreach (var obj in HitObjectContainer.AliveObjects)
|
|
||||||
{
|
|
||||||
if (obj.HitObject.StartTime >= targetTime)
|
|
||||||
yield break;
|
|
||||||
|
|
||||||
switch (obj)
|
|
||||||
{
|
|
||||||
case DrawableSpinner:
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case DrawableSlider slider:
|
|
||||||
yield return slider.HeadCircle;
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
yield return obj;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
|
|
||||||
protected override void OnNewDrawableHitObject(DrawableHitObject drawable)
|
protected override void OnNewDrawableHitObject(DrawableHitObject drawable)
|
||||||
{
|
{
|
||||||
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable;
|
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.CheckHittable;
|
||||||
|
|
||||||
Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}");
|
Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}");
|
||||||
drawable.OnLoadComplete += onDrawableHitObjectLoaded;
|
drawable.OnLoadComplete += onDrawableHitObjectLoaded;
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
@ -22,11 +21,14 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class StartTimeOrderedHitPolicy : IHitPolicy
|
public class StartTimeOrderedHitPolicy : IHitPolicy
|
||||||
{
|
{
|
||||||
public IHitObjectContainer HitObjectContainer { get; set; }
|
public IHitObjectContainer? HitObjectContainer { get; set; }
|
||||||
|
|
||||||
public bool IsHittable(DrawableHitObject hitObject, double time)
|
public ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult _)
|
||||||
{
|
{
|
||||||
DrawableHitObject blockingObject = null;
|
if (HitObjectContainer == null)
|
||||||
|
throw new InvalidOperationException($"{nameof(HitObjectContainer)} should be set before {nameof(CheckHittable)} is called.");
|
||||||
|
|
||||||
|
DrawableHitObject? blockingObject = null;
|
||||||
|
|
||||||
foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
|
foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
|
||||||
{
|
{
|
||||||
@ -36,22 +38,25 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
|
|
||||||
// If there is no previous hitobject, allow the hit.
|
// If there is no previous hitobject, allow the hit.
|
||||||
if (blockingObject == null)
|
if (blockingObject == null)
|
||||||
return true;
|
return ClickAction.Hit;
|
||||||
|
|
||||||
// A hit is allowed if:
|
// A hit is allowed if:
|
||||||
// 1. The last blocking hitobject has been judged.
|
// 1. The last blocking hitobject has been judged.
|
||||||
// 2. The current time is after the last hitobject's start time.
|
// 2. The current time is after the last hitobject's start time.
|
||||||
// Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245).
|
// Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245).
|
||||||
return blockingObject.Judged || time >= blockingObject.HitObject.StartTime;
|
return (blockingObject.Judged || time >= blockingObject.HitObject.StartTime) ? ClickAction.Hit : ClickAction.Shake;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void HandleHit(DrawableHitObject hitObject)
|
public void HandleHit(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
|
if (HitObjectContainer == null)
|
||||||
|
throw new InvalidOperationException($"{nameof(HitObjectContainer)} should be set before {nameof(HandleHit)} is called.");
|
||||||
|
|
||||||
// Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners).
|
// Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners).
|
||||||
if (!hitObjectCanBlockFutureHits(hitObject))
|
if (!hitObjectCanBlockFutureHits(hitObject))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset))
|
if (CheckHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset, hitObject.Result.Type) != ClickAction.Hit)
|
||||||
throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
|
throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
|
||||||
|
|
||||||
// Miss all hitobjects prior to the hit one.
|
// Miss all hitobjects prior to the hit one.
|
||||||
@ -74,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
|
|
||||||
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
|
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
|
||||||
{
|
{
|
||||||
foreach (var obj in HitObjectContainer.AliveObjects)
|
foreach (var obj in HitObjectContainer!.AliveObjects)
|
||||||
{
|
{
|
||||||
if (obj.HitObject.StartTime >= targetTime)
|
if (obj.HitObject.StartTime >= targetTime)
|
||||||
yield break;
|
yield break;
|
||||||
|
185
osu.Game.Rulesets.Taiko.Tests/TestSceneScoring.cs
Normal file
185
osu.Game.Rulesets.Taiko.Tests/TestSceneScoring.cs
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
// 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.Judgements;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Taiko.Judgements;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.Scoring;
|
||||||
|
using osu.Game.Tests.Visual.Gameplay;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public partial class TestSceneScoring : ScoringTestScene
|
||||||
|
{
|
||||||
|
private Bindable<double> scoreMultiplier { get; } = new BindableDouble
|
||||||
|
{
|
||||||
|
Default = 4,
|
||||||
|
Value = 4
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override IBeatmap CreateBeatmap(int maxCombo)
|
||||||
|
{
|
||||||
|
var beatmap = new TaikoBeatmap();
|
||||||
|
for (int i = 0; i < maxCombo; ++i)
|
||||||
|
beatmap.HitObjects.Add(new Hit());
|
||||||
|
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 TaikoProcessorBasedScoringAlgorithm(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 });
|
||||||
|
});
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int base_great = 300;
|
||||||
|
private const int base_ok = 150;
|
||||||
|
|
||||||
|
private class ScoreV1 : IScoringAlgorithm
|
||||||
|
{
|
||||||
|
private int currentCombo;
|
||||||
|
|
||||||
|
public BindableDouble ScoreMultiplier { get; } = new BindableDouble();
|
||||||
|
|
||||||
|
public void ApplyHit() => applyHitV1(base_great);
|
||||||
|
|
||||||
|
public void ApplyNonPerfect() => applyHitV1(base_ok);
|
||||||
|
|
||||||
|
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)((baseScore / 35) * 2 * (ScoreMultiplier.Value + 1)) * (Math.Min(100, currentCombo) / 10);
|
||||||
|
|
||||||
|
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(base_great);
|
||||||
|
|
||||||
|
public void ApplyNonPerfect() => applyHitV2(base_ok);
|
||||||
|
|
||||||
|
private void applyHitV2(int baseScore)
|
||||||
|
{
|
||||||
|
maxBaseScore += base_great;
|
||||||
|
currentBaseScore += baseScore;
|
||||||
|
|
||||||
|
currentHits++;
|
||||||
|
|
||||||
|
// `base_great` is INTENTIONALLY used above here instead of `baseScore`
|
||||||
|
// see `BaseHitValue` override in `ScoreChangeTaiko` on stable
|
||||||
|
comboPortion += base_great * Math.Min(Math.Max(0.5, Math.Log(++currentCombo, combo_base)), Math.Log(400, combo_base));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyMiss()
|
||||||
|
{
|
||||||
|
currentHits++;
|
||||||
|
maxBaseScore += base_great;
|
||||||
|
currentCombo = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long TotalScore
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
double accuracy = currentBaseScore / maxBaseScore;
|
||||||
|
|
||||||
|
return (int)Math.Round
|
||||||
|
(
|
||||||
|
250000 * comboPortion / comboPortionMax +
|
||||||
|
750000 * Math.Pow(accuracy, 3.6) * ((double)currentHits / maxCombo)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TaikoProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm
|
||||||
|
{
|
||||||
|
public TaikoProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode)
|
||||||
|
: base(beatmap, mode)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor();
|
||||||
|
protected override JudgementResult CreatePerfectJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great };
|
||||||
|
protected override JudgementResult CreateNonPerfectJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok };
|
||||||
|
protected override JudgementResult CreateMissJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { 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>
|
||||||
|
@ -14,6 +14,7 @@ using JetBrains.Annotations;
|
|||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Beatmaps.Formats;
|
using osu.Game.Beatmaps.Formats;
|
||||||
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Beatmaps
|
namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||||
{
|
{
|
||||||
@ -64,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
|||||||
{
|
{
|
||||||
if (hitObject is not IHasSliderVelocity hasSliderVelocity) continue;
|
if (hitObject is not IHasSliderVelocity hasSliderVelocity) continue;
|
||||||
|
|
||||||
double nextScrollSpeed = hasSliderVelocity.SliderVelocity;
|
double nextScrollSpeed = hasSliderVelocity.SliderVelocityMultiplier;
|
||||||
EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime);
|
EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime);
|
||||||
|
|
||||||
if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision))
|
if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision))
|
||||||
@ -186,10 +187,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
|||||||
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
|
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
|
||||||
|
|
||||||
double beatLength;
|
double beatLength;
|
||||||
if (obj.LegacyBpmMultiplier.HasValue)
|
|
||||||
beatLength = timingPoint.BeatLength * obj.LegacyBpmMultiplier.Value;
|
if (obj is IHasSliderVelocity hasSliderVelocity)
|
||||||
else if (obj is IHasSliderVelocity hasSliderVelocity)
|
beatLength = LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(hasSliderVelocity, timingPoint, TaikoRuleset.SHORT_NAME);
|
||||||
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
|
|
||||||
else
|
else
|
||||||
beatLength = timingPoint.BeatLength;
|
beatLength = timingPoint.BeatLength;
|
||||||
|
|
||||||
|
@ -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()
|
||||||
{
|
{
|
||||||
@ -1024,10 +1056,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1));
|
Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1));
|
||||||
Assert.That(controlPoints.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(1));
|
Assert.That(controlPoints.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(1));
|
||||||
|
|
||||||
#pragma warning disable 618
|
Assert.That(controlPoints.DifficultyPointAt(2000).GenerateTicks, Is.False);
|
||||||
Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(2000)).GenerateTicks, Is.False);
|
Assert.That(controlPoints.DifficultyPointAt(3000).GenerateTicks, Is.True);
|
||||||
Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(3000)).GenerateTicks, Is.True);
|
|
||||||
#pragma warning restore 618
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,8 +32,26 @@ namespace osu.Game.Tests.Chat
|
|||||||
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a gopher://really-old-protocol we don't support." });
|
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a gopher://really-old-protocol we don't support." });
|
||||||
|
|
||||||
Assert.AreEqual(result.Content, result.DisplayContent);
|
Assert.AreEqual(result.Content, result.DisplayContent);
|
||||||
Assert.AreEqual(1, result.Links.Count);
|
Assert.AreEqual(0, result.Links.Count);
|
||||||
Assert.AreEqual("gopher://really-old-protocol", result.Links[0].Url);
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestFakeProtocolLink()
|
||||||
|
{
|
||||||
|
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a osunotarealprotocol://completely-made-up-protocol we don't support." });
|
||||||
|
|
||||||
|
Assert.AreEqual(result.Content, result.DisplayContent);
|
||||||
|
Assert.AreEqual(0, result.Links.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSupportedProtocolLinkParsing()
|
||||||
|
{
|
||||||
|
Message result = MessageFormatter.FormatMessage(new Message { Content = "forgotspacehttps://dev.ppy.sh joinmyosump://12345 jointheosu://chan/#english" });
|
||||||
|
|
||||||
|
Assert.AreEqual("https://dev.ppy.sh", result.Links[0].Url);
|
||||||
|
Assert.AreEqual("osump://12345", result.Links[1].Url);
|
||||||
|
Assert.AreEqual("osu://chan/#english", result.Links[2].Url);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -78,7 +78,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
assertSnapDistance(100, new Slider
|
assertSnapDistance(100, new Slider
|
||||||
{
|
{
|
||||||
SliderVelocity = multiplier
|
SliderVelocityMultiplier = multiplier
|
||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
assertSnapDistance(100 * multiplier, new Slider
|
assertSnapDistance(100 * multiplier, new Slider
|
||||||
{
|
{
|
||||||
SliderVelocity = multiplier
|
SliderVelocityMultiplier = multiplier
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
|
|
||||||
var referenceObject = new Slider
|
var referenceObject = new Slider
|
||||||
{
|
{
|
||||||
SliderVelocity = slider_velocity
|
SliderVelocityMultiplier = slider_velocity
|
||||||
};
|
};
|
||||||
|
|
||||||
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
|
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
|
||||||
|
@ -13,7 +13,7 @@ layout(location = 4) out mediump vec2 v_BlendRange;
|
|||||||
void main(void)
|
void main(void)
|
||||||
{
|
{
|
||||||
// Transform from screen space to masking space.
|
// Transform from screen space to masking space.
|
||||||
highp vec3 maskingPos = g_MaskingInfo.ToMaskingSpace * vec3(m_Position, 1.0);
|
highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
|
||||||
v_MaskingPosition = maskingPos.xy / maskingPos.z;
|
v_MaskingPosition = maskingPos.xy / maskingPos.z;
|
||||||
|
|
||||||
v_Colour = m_Colour;
|
v_Colour = m_Colour;
|
||||||
|
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:
|
@ -260,6 +260,12 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
|||||||
AddStep($"set {scheme} scheme", () => Child = createContent(scheme, creationFunc));
|
AddStep($"set {scheme} scheme", () => Child = createContent(scheme, creationFunc));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNano()
|
||||||
|
{
|
||||||
|
createTestCase(beatmapSetInfo => new BeatmapCardNano(beatmapSetInfo));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestNormal()
|
public void TestNormal()
|
||||||
{
|
{
|
||||||
|
@ -47,7 +47,35 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
|||||||
pill.AutoSizeAxes = Axes.Y;
|
pill.AutoSizeAxes = Axes.Y;
|
||||||
pill.Width = 90;
|
pill.Width = 90;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
AddStep("unset fixed width", () => statusPills.ForEach(pill => pill.AutoSizeAxes = Axes.Both));
|
AddStep("unset fixed width", () => statusPills.ForEach(pill => pill.AutoSizeAxes = Axes.Both));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestChangeLabels()
|
||||||
|
{
|
||||||
|
AddStep("Change labels", () =>
|
||||||
|
{
|
||||||
|
foreach (var pill in this.ChildrenOfType<BeatmapSetOnlineStatusPill>())
|
||||||
|
{
|
||||||
|
switch (pill.Status)
|
||||||
|
{
|
||||||
|
// cycle at end
|
||||||
|
case BeatmapOnlineStatus.Loved:
|
||||||
|
pill.Status = BeatmapOnlineStatus.LocallyModified;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// skip none
|
||||||
|
case BeatmapOnlineStatus.LocallyModified:
|
||||||
|
pill.Status = BeatmapOnlineStatus.Graveyard;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
pill.Status = (pill.Status + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
new PathControlPoint(new Vector2(100, 0))
|
new PathControlPoint(new Vector2(100, 0))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SliderVelocity = 2
|
SliderVelocityMultiplier = 2
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddStep("unify slider velocity", () =>
|
AddStep("unify slider velocity", () =>
|
||||||
{
|
{
|
||||||
foreach (var h in EditorBeatmap.HitObjects.OfType<IHasSliderVelocity>())
|
foreach (var h in EditorBeatmap.HitObjects.OfType<IHasSliderVelocity>())
|
||||||
h.SliderVelocity = 1.5;
|
h.SliderVelocityMultiplier = 1.5;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||||
@ -194,7 +194,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () =>
|
private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () =>
|
||||||
{
|
{
|
||||||
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
|
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
|
||||||
return h is IHasSliderVelocity hasSliderVelocity && hasSliderVelocity.SliderVelocity == velocity;
|
return h is IHasSliderVelocity hasSliderVelocity && hasSliderVelocity.SliderVelocityMultiplier == velocity;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -1,499 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Graphics.Cursor;
|
|
||||||
using osu.Framework.Graphics.Shapes;
|
|
||||||
using osu.Framework.Graphics.Sprites;
|
|
||||||
using osu.Framework.Input.Events;
|
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osu.Game.Graphics.Containers;
|
|
||||||
using osu.Game.Graphics.Sprites;
|
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
using osu.Game.Overlays.Settings;
|
|
||||||
using osu.Game.Rulesets.Osu.Scoring;
|
|
||||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
|
||||||
using osu.Game.Rulesets.Osu.Judgements;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
using osu.Game.Rulesets.Scoring;
|
|
||||||
using osu.Game.Scoring.Legacy;
|
|
||||||
using osuTK;
|
|
||||||
using osuTK.Graphics;
|
|
||||||
using osuTK.Input;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
|
||||||
{
|
|
||||||
public partial class TestSceneScoring : OsuTestScene
|
|
||||||
{
|
|
||||||
private GraphContainer graphs = null!;
|
|
||||||
private SettingsSlider<int> sliderMaxCombo = null!;
|
|
||||||
|
|
||||||
private FillFlowContainer legend = null!;
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestBasic()
|
|
||||||
{
|
|
||||||
AddStep("setup tests", () =>
|
|
||||||
{
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new GridContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
RowDimensions = new[]
|
|
||||||
{
|
|
||||||
new Dimension(),
|
|
||||||
new Dimension(GridSizeMode.AutoSize),
|
|
||||||
new Dimension(GridSizeMode.AutoSize),
|
|
||||||
},
|
|
||||||
Content = new[]
|
|
||||||
{
|
|
||||||
new Drawable[]
|
|
||||||
{
|
|
||||||
graphs = new GraphContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
new Drawable[]
|
|
||||||
{
|
|
||||||
legend = new FillFlowContainer
|
|
||||||
{
|
|
||||||
Padding = new MarginPadding(20),
|
|
||||||
Direction = FillDirection.Full,
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
new Drawable[]
|
|
||||||
{
|
|
||||||
new FillFlowContainer
|
|
||||||
{
|
|
||||||
Padding = new MarginPadding(20),
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Direction = FillDirection.Full,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
sliderMaxCombo = new SettingsSlider<int>
|
|
||||||
{
|
|
||||||
Width = 0.5f,
|
|
||||||
TransferValueOnCommit = true,
|
|
||||||
Current = new BindableInt(1024)
|
|
||||||
{
|
|
||||||
MinValue = 96,
|
|
||||||
MaxValue = 8192,
|
|
||||||
},
|
|
||||||
LabelText = "max combo",
|
|
||||||
},
|
|
||||||
new OsuTextFlowContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
Width = 0.5f,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Text = $"Left click to add miss\nRight click to add OK/{base_ok}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
sliderMaxCombo.Current.BindValueChanged(_ => rerun());
|
|
||||||
|
|
||||||
graphs.MissLocations.BindCollectionChanged((_, __) => rerun());
|
|
||||||
graphs.NonPerfectLocations.BindCollectionChanged((_, __) => rerun());
|
|
||||||
|
|
||||||
graphs.MaxCombo.BindTo(sliderMaxCombo.Current);
|
|
||||||
|
|
||||||
rerun();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private const int base_great = 300;
|
|
||||||
private const int base_ok = 100;
|
|
||||||
|
|
||||||
private void rerun()
|
|
||||||
{
|
|
||||||
graphs.Clear();
|
|
||||||
legend.Clear();
|
|
||||||
|
|
||||||
runForProcessor("lazer-standardised", Color4.YellowGreen, new OsuScoreProcessor(), ScoringMode.Standardised);
|
|
||||||
runForProcessor("lazer-classic", Color4.MediumPurple, new OsuScoreProcessor(), ScoringMode.Classic);
|
|
||||||
|
|
||||||
runScoreV1();
|
|
||||||
runScoreV2();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void runScoreV1()
|
|
||||||
{
|
|
||||||
int totalScore = 0;
|
|
||||||
int currentCombo = 0;
|
|
||||||
|
|
||||||
void applyHitV1(int baseScore)
|
|
||||||
{
|
|
||||||
if (baseScore == 0)
|
|
||||||
{
|
|
||||||
currentCombo = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const float score_multiplier = 1;
|
|
||||||
|
|
||||||
totalScore += baseScore;
|
|
||||||
|
|
||||||
// combo multiplier
|
|
||||||
// ReSharper disable once PossibleLossOfFraction
|
|
||||||
totalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * score_multiplier));
|
|
||||||
|
|
||||||
currentCombo++;
|
|
||||||
}
|
|
||||||
|
|
||||||
runForAlgorithm("ScoreV1 (classic)", Color4.Purple,
|
|
||||||
() => applyHitV1(base_great),
|
|
||||||
() => applyHitV1(base_ok),
|
|
||||||
() => applyHitV1(0),
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
// Arbitrary value chosen towards the upper range.
|
|
||||||
const double score_multiplier = 4;
|
|
||||||
|
|
||||||
return (int)(totalScore * score_multiplier);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void runScoreV2()
|
|
||||||
{
|
|
||||||
int maxCombo = sliderMaxCombo.Current.Value;
|
|
||||||
|
|
||||||
int currentCombo = 0;
|
|
||||||
double comboPortion = 0;
|
|
||||||
double currentBaseScore = 0;
|
|
||||||
double maxBaseScore = 0;
|
|
||||||
int currentHits = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < maxCombo; i++)
|
|
||||||
applyHitV2(base_great);
|
|
||||||
|
|
||||||
double comboPortionMax = comboPortion;
|
|
||||||
|
|
||||||
currentCombo = 0;
|
|
||||||
comboPortion = 0;
|
|
||||||
currentBaseScore = 0;
|
|
||||||
maxBaseScore = 0;
|
|
||||||
currentHits = 0;
|
|
||||||
|
|
||||||
void applyHitV2(int baseScore)
|
|
||||||
{
|
|
||||||
maxBaseScore += base_great;
|
|
||||||
currentBaseScore += baseScore;
|
|
||||||
comboPortion += baseScore * (1 + ++currentCombo / 10.0);
|
|
||||||
|
|
||||||
currentHits++;
|
|
||||||
}
|
|
||||||
|
|
||||||
runForAlgorithm("ScoreV2", Color4.OrangeRed,
|
|
||||||
() => applyHitV2(base_great),
|
|
||||||
() => applyHitV2(base_ok),
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
currentHits++;
|
|
||||||
maxBaseScore += base_great;
|
|
||||||
currentCombo = 0;
|
|
||||||
}, () =>
|
|
||||||
{
|
|
||||||
double accuracy = currentBaseScore / maxBaseScore;
|
|
||||||
|
|
||||||
return (int)Math.Round
|
|
||||||
(
|
|
||||||
700000 * comboPortion / comboPortionMax +
|
|
||||||
300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void runForProcessor(string name, Color4 colour, ScoreProcessor processor, ScoringMode mode)
|
|
||||||
{
|
|
||||||
int maxCombo = sliderMaxCombo.Current.Value;
|
|
||||||
|
|
||||||
var beatmap = new OsuBeatmap();
|
|
||||||
for (int i = 0; i < maxCombo; i++)
|
|
||||||
beatmap.HitObjects.Add(new HitCircle());
|
|
||||||
|
|
||||||
processor.ApplyBeatmap(beatmap);
|
|
||||||
|
|
||||||
runForAlgorithm(name, colour,
|
|
||||||
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }),
|
|
||||||
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }),
|
|
||||||
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }),
|
|
||||||
() => processor.GetDisplayScore(mode));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func<long> getTotalScore)
|
|
||||||
{
|
|
||||||
int maxCombo = sliderMaxCombo.Current.Value;
|
|
||||||
|
|
||||||
List<float> results = new List<float>();
|
|
||||||
|
|
||||||
for (int i = 0; i < maxCombo; i++)
|
|
||||||
{
|
|
||||||
if (graphs.MissLocations.Contains(i))
|
|
||||||
applyMiss();
|
|
||||||
else if (graphs.NonPerfectLocations.Contains(i))
|
|
||||||
applyNonPerfect();
|
|
||||||
else
|
|
||||||
applyHit();
|
|
||||||
|
|
||||||
results.Add(getTotalScore());
|
|
||||||
}
|
|
||||||
|
|
||||||
graphs.Add(new LineGraph
|
|
||||||
{
|
|
||||||
Name = name,
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
LineColour = colour,
|
|
||||||
Values = results
|
|
||||||
});
|
|
||||||
|
|
||||||
legend.Add(new OsuSpriteText
|
|
||||||
{
|
|
||||||
Colour = colour,
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
Width = 0.5f,
|
|
||||||
Text = $"{FontAwesome.Solid.Circle.Icon} {name}"
|
|
||||||
});
|
|
||||||
|
|
||||||
legend.Add(new OsuSpriteText
|
|
||||||
{
|
|
||||||
Colour = colour,
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
Width = 0.5f,
|
|
||||||
Text = $"final score {getTotalScore():#,0}"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class GraphContainer : Container, IHasCustomTooltip<IEnumerable<LineGraph>>
|
|
||||||
{
|
|
||||||
public readonly BindableList<double> MissLocations = new BindableList<double>();
|
|
||||||
public readonly BindableList<double> NonPerfectLocations = new BindableList<double>();
|
|
||||||
|
|
||||||
public Bindable<int> MaxCombo = new Bindable<int>();
|
|
||||||
|
|
||||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
|
||||||
|
|
||||||
private readonly Box hoverLine;
|
|
||||||
|
|
||||||
private readonly Container missLines;
|
|
||||||
private readonly Container verticalGridLines;
|
|
||||||
|
|
||||||
public int CurrentHoverCombo { get; private set; }
|
|
||||||
|
|
||||||
public GraphContainer()
|
|
||||||
{
|
|
||||||
InternalChild = new Container
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new Box
|
|
||||||
{
|
|
||||||
Colour = OsuColour.Gray(0.1f),
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
},
|
|
||||||
verticalGridLines = new Container
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
},
|
|
||||||
hoverLine = new Box
|
|
||||||
{
|
|
||||||
Colour = Color4.Yellow,
|
|
||||||
RelativeSizeAxes = Axes.Y,
|
|
||||||
Origin = Anchor.TopCentre,
|
|
||||||
Alpha = 0,
|
|
||||||
Width = 1,
|
|
||||||
},
|
|
||||||
missLines = new Container
|
|
||||||
{
|
|
||||||
Alpha = 0.6f,
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
},
|
|
||||||
Content,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
MissLocations.BindCollectionChanged((_, _) => updateMissLocations());
|
|
||||||
NonPerfectLocations.BindCollectionChanged((_, _) => updateMissLocations());
|
|
||||||
|
|
||||||
MaxCombo.BindValueChanged(_ =>
|
|
||||||
{
|
|
||||||
updateMissLocations();
|
|
||||||
updateVerticalGridLines();
|
|
||||||
}, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateVerticalGridLines()
|
|
||||||
{
|
|
||||||
verticalGridLines.Clear();
|
|
||||||
|
|
||||||
for (int i = 0; i < MaxCombo.Value; i++)
|
|
||||||
{
|
|
||||||
if (i % 100 == 0)
|
|
||||||
{
|
|
||||||
verticalGridLines.AddRange(new Drawable[]
|
|
||||||
{
|
|
||||||
new Box
|
|
||||||
{
|
|
||||||
Colour = OsuColour.Gray(0.2f),
|
|
||||||
Origin = Anchor.TopCentre,
|
|
||||||
Width = 1,
|
|
||||||
RelativeSizeAxes = Axes.Y,
|
|
||||||
RelativePositionAxes = Axes.X,
|
|
||||||
X = (float)i / MaxCombo.Value,
|
|
||||||
},
|
|
||||||
new OsuSpriteText
|
|
||||||
{
|
|
||||||
RelativePositionAxes = Axes.X,
|
|
||||||
X = (float)i / MaxCombo.Value,
|
|
||||||
Anchor = Anchor.BottomLeft,
|
|
||||||
Origin = Anchor.BottomLeft,
|
|
||||||
Text = $"{i:#,0}",
|
|
||||||
Rotation = -30,
|
|
||||||
Y = -20,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateMissLocations()
|
|
||||||
{
|
|
||||||
missLines.Clear();
|
|
||||||
|
|
||||||
foreach (int miss in MissLocations)
|
|
||||||
{
|
|
||||||
missLines.Add(new Box
|
|
||||||
{
|
|
||||||
Colour = Color4.Red,
|
|
||||||
Origin = Anchor.TopCentre,
|
|
||||||
Width = 1,
|
|
||||||
RelativeSizeAxes = Axes.Y,
|
|
||||||
RelativePositionAxes = Axes.X,
|
|
||||||
X = (float)miss / MaxCombo.Value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (int miss in NonPerfectLocations)
|
|
||||||
{
|
|
||||||
missLines.Add(new Box
|
|
||||||
{
|
|
||||||
Colour = Color4.Orange,
|
|
||||||
Origin = Anchor.TopCentre,
|
|
||||||
Width = 1,
|
|
||||||
RelativeSizeAxes = Axes.Y,
|
|
||||||
RelativePositionAxes = Axes.X,
|
|
||||||
X = (float)miss / MaxCombo.Value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
|
||||||
{
|
|
||||||
hoverLine.Show();
|
|
||||||
return base.OnHover(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnHoverLost(HoverLostEvent e)
|
|
||||||
{
|
|
||||||
hoverLine.Hide();
|
|
||||||
base.OnHoverLost(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
|
||||||
{
|
|
||||||
CurrentHoverCombo = (int)(e.MousePosition.X / DrawWidth * MaxCombo.Value);
|
|
||||||
|
|
||||||
hoverLine.X = e.MousePosition.X;
|
|
||||||
return base.OnMouseMove(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
|
||||||
{
|
|
||||||
if (e.Button == MouseButton.Left)
|
|
||||||
MissLocations.Add(CurrentHoverCombo);
|
|
||||||
else
|
|
||||||
NonPerfectLocations.Add(CurrentHoverCombo);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private GraphTooltip? tooltip;
|
|
||||||
|
|
||||||
public ITooltip<IEnumerable<LineGraph>> GetCustomTooltip() => tooltip ??= new GraphTooltip(this);
|
|
||||||
|
|
||||||
public IEnumerable<LineGraph> TooltipContent => Content.OfType<LineGraph>();
|
|
||||||
|
|
||||||
public partial class GraphTooltip : CompositeDrawable, ITooltip<IEnumerable<LineGraph>>
|
|
||||||
{
|
|
||||||
private readonly GraphContainer graphContainer;
|
|
||||||
|
|
||||||
private readonly OsuTextFlowContainer textFlow;
|
|
||||||
|
|
||||||
public GraphTooltip(GraphContainer graphContainer)
|
|
||||||
{
|
|
||||||
this.graphContainer = graphContainer;
|
|
||||||
AutoSizeAxes = Axes.Both;
|
|
||||||
|
|
||||||
Masking = true;
|
|
||||||
CornerRadius = 10;
|
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
|
||||||
{
|
|
||||||
new Box
|
|
||||||
{
|
|
||||||
Colour = OsuColour.Gray(0.15f),
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
},
|
|
||||||
textFlow = new OsuTextFlowContainer
|
|
||||||
{
|
|
||||||
Colour = Color4.White,
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Padding = new MarginPadding(10),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private int? lastContentCombo;
|
|
||||||
|
|
||||||
public void SetContent(IEnumerable<LineGraph> content)
|
|
||||||
{
|
|
||||||
int relevantCombo = graphContainer.CurrentHoverCombo;
|
|
||||||
|
|
||||||
if (lastContentCombo == relevantCombo)
|
|
||||||
return;
|
|
||||||
|
|
||||||
lastContentCombo = relevantCombo;
|
|
||||||
textFlow.Clear();
|
|
||||||
|
|
||||||
textFlow.AddParagraph($"At combo {relevantCombo}:");
|
|
||||||
|
|
||||||
foreach (var graph in content)
|
|
||||||
{
|
|
||||||
float valueAtHover = graph.Values.ElementAt(relevantCombo);
|
|
||||||
float ofTotal = valueAtHover / graph.Values.Last();
|
|
||||||
|
|
||||||
textFlow.AddParagraph($"{graph.Name}: {valueAtHover:#,0} ({ofTotal * 100:N0}% of final)\n", st => st.Colour = graph.LineColour);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Move(Vector2 pos) => this.MoveTo(pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,6 +9,7 @@ using osu.Framework.Extensions.ObjectExtensions;
|
|||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Rulesets.Mania;
|
using osu.Game.Rulesets.Mania;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
@ -16,6 +17,7 @@ using osu.Game.Screens.Edit;
|
|||||||
using osu.Game.Screens.Edit.GameplayTest;
|
using osu.Game.Screens.Edit.GameplayTest;
|
||||||
using osu.Game.Screens.Menu;
|
using osu.Game.Screens.Menu;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
|
using osu.Game.Screens.Select.Filter;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
@ -203,6 +205,33 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying);
|
AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase(SortMode.Title)]
|
||||||
|
[TestCase(SortMode.Difficulty)]
|
||||||
|
public void TestSelectionRetainedOnExit(SortMode sortMode)
|
||||||
|
{
|
||||||
|
BeatmapSetInfo beatmapSet = null!;
|
||||||
|
|
||||||
|
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||||
|
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||||
|
|
||||||
|
AddStep($"set sort mode to {sortMode}", () => Game.LocalConfig.SetValue(OsuSetting.SongSelectSortingMode, sortMode));
|
||||||
|
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||||
|
AddUntilStep("wait for song select",
|
||||||
|
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||||
|
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||||
|
&& songSelect.IsLoaded);
|
||||||
|
|
||||||
|
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||||
|
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||||
|
|
||||||
|
AddStep("exit editor", () => InputManager.Key(Key.Escape));
|
||||||
|
AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor);
|
||||||
|
|
||||||
|
AddUntilStep("selection retained on song select",
|
||||||
|
() => Game.Beatmap.Value.BeatmapInfo.ID,
|
||||||
|
() => Is.EqualTo(beatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0).ID));
|
||||||
|
}
|
||||||
|
|
||||||
private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType<EditorBeatmap>().Single();
|
private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType<EditorBeatmap>().Single();
|
||||||
|
|
||||||
private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;
|
private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;
|
||||||
|
@ -64,6 +64,9 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
addMessageWithChecks("test!");
|
addMessageWithChecks("test!");
|
||||||
addMessageWithChecks("dev.ppy.sh!");
|
addMessageWithChecks("dev.ppy.sh!");
|
||||||
addMessageWithChecks("https://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
|
addMessageWithChecks("https://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
|
||||||
|
addMessageWithChecks("http://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
|
||||||
|
addMessageWithChecks("forgothttps://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
|
||||||
|
addMessageWithChecks("forgothttp://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
|
||||||
addMessageWithChecks("00:12:345 (1,2) - Test?", 1, expectedActions: LinkAction.OpenEditorTimestamp);
|
addMessageWithChecks("00:12:345 (1,2) - Test?", 1, expectedActions: LinkAction.OpenEditorTimestamp);
|
||||||
addMessageWithChecks("Wiki link for tasty [[Performance Points]]", 1, expectedActions: LinkAction.OpenWiki);
|
addMessageWithChecks("Wiki link for tasty [[Performance Points]]", 1, expectedActions: LinkAction.OpenWiki);
|
||||||
addMessageWithChecks("(osu forums)[https://dev.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External);
|
addMessageWithChecks("(osu forums)[https://dev.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External);
|
||||||
@ -84,9 +87,11 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
addMessageWithChecks("feels important", 0, true, true);
|
addMessageWithChecks("feels important", 0, true, true);
|
||||||
addMessageWithChecks("likes to post this [https://dev.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External);
|
addMessageWithChecks("likes to post this [https://dev.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External);
|
||||||
addMessageWithChecks("Join my multiplayer game osump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
|
addMessageWithChecks("Join my multiplayer game osump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
|
||||||
|
addMessageWithChecks("Join my multiplayer gameosump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
|
||||||
addMessageWithChecks("Join my [multiplayer game](osump://12346).", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
|
addMessageWithChecks("Join my [multiplayer game](osump://12346).", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
|
||||||
addMessageWithChecks($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", 1, expectedActions: LinkAction.OpenChannel);
|
addMessageWithChecks($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", 1, expectedActions: LinkAction.OpenChannel);
|
||||||
addMessageWithChecks($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel);
|
addMessageWithChecks($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel);
|
||||||
|
addMessageWithChecks($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel);
|
||||||
addMessageWithChecks("Join my #english or #japanese channels.", 2, expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel });
|
addMessageWithChecks("Join my #english or #japanese channels.", 2, expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel });
|
||||||
addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", 1, expectedActions: LinkAction.OpenChannel);
|
addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", 1, expectedActions: LinkAction.OpenChannel);
|
||||||
addMessageWithChecks("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20");
|
addMessageWithChecks("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20");
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.API.Requests;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Online
|
||||||
|
{
|
||||||
|
public partial class TestSceneReplayMissingBeatmap : OsuGameTestScene
|
||||||
|
{
|
||||||
|
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSceneMissingBeatmapWithOnlineAvailable()
|
||||||
|
{
|
||||||
|
var beatmap = new APIBeatmap
|
||||||
|
{
|
||||||
|
OnlineBeatmapSetID = 173612,
|
||||||
|
BeatmapSet = new APIBeatmapSet
|
||||||
|
{
|
||||||
|
Title = "FREEDOM Dive",
|
||||||
|
Artist = "xi",
|
||||||
|
Covers = new BeatmapSetOnlineCovers
|
||||||
|
{
|
||||||
|
Card = "https://assets.ppy.sh/beatmaps/173612/covers/card@2x.jpg"
|
||||||
|
},
|
||||||
|
OnlineID = 173612
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setupBeatmapResponse(beatmap);
|
||||||
|
|
||||||
|
AddStep("import score", () =>
|
||||||
|
{
|
||||||
|
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
|
||||||
|
{
|
||||||
|
var importTask = new ImportTask(resourceStream, "replay.osr");
|
||||||
|
|
||||||
|
Game.ScoreManager.Import(new[] { importTask });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("Replay missing notification show", () => Game.Notifications.ChildrenOfType<MissingBeatmapNotification>().Any());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSceneMissingBeatmapWithOnlineUnavailable()
|
||||||
|
{
|
||||||
|
setupFailedResponse();
|
||||||
|
|
||||||
|
AddStep("import score", () =>
|
||||||
|
{
|
||||||
|
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
|
||||||
|
{
|
||||||
|
var importTask = new ImportTask(resourceStream, "replay.osr");
|
||||||
|
|
||||||
|
Game.ScoreManager.Import(new[] { importTask });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("Replay missing notification not show", () => !Game.Notifications.ChildrenOfType<MissingBeatmapNotification>().Any());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupBeatmapResponse(APIBeatmap b)
|
||||||
|
=> AddStep("setup response", () =>
|
||||||
|
{
|
||||||
|
dummyAPI.HandleRequest = request =>
|
||||||
|
{
|
||||||
|
if (request is GetBeatmapRequest getBeatmapRequest)
|
||||||
|
{
|
||||||
|
getBeatmapRequest.TriggerSuccess(b);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
private void setupFailedResponse()
|
||||||
|
=> AddStep("setup failed response", () =>
|
||||||
|
{
|
||||||
|
dummyAPI.HandleRequest = request =>
|
||||||
|
{
|
||||||
|
request.TriggerFailure(new WebException());
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -110,5 +110,31 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
}
|
}
|
||||||
}, new OsuRuleset().RulesetInfo));
|
}, new OsuRuleset().RulesetInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPreviousUsernames()
|
||||||
|
{
|
||||||
|
AddStep("Show user w/ previous usernames", () => header.User.Value = new UserProfileData(new APIUser
|
||||||
|
{
|
||||||
|
Id = 727,
|
||||||
|
Username = "SomeoneIndecisive",
|
||||||
|
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg",
|
||||||
|
Groups = new[]
|
||||||
|
{
|
||||||
|
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
|
||||||
|
},
|
||||||
|
Statistics = new UserStatistics
|
||||||
|
{
|
||||||
|
IsRanked = false,
|
||||||
|
// web will sometimes return non-empty rank history even for unranked users.
|
||||||
|
RankHistory = new APIRankHistory
|
||||||
|
{
|
||||||
|
Mode = @"osu",
|
||||||
|
Data = Enumerable.Range(2345, 85).ToArray()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreviousUsernames = new[] { "tsrk.", "quoicoubeh", "apagnan", "epita" }
|
||||||
|
}, new OsuRuleset().RulesetInfo));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,14 +134,16 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
|
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
|
||||||
Description = "Outstanding help by being a voluntary test subject.",
|
Description = "Outstanding help by being a voluntary test subject.",
|
||||||
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.jpg",
|
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor-new@2x.png",
|
||||||
|
ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor-new.png",
|
||||||
Url = "https://osu.ppy.sh/wiki/en/People/Community_Contributors",
|
Url = "https://osu.ppy.sh/wiki/en/People/Community_Contributors",
|
||||||
},
|
},
|
||||||
new Badge
|
new Badge
|
||||||
{
|
{
|
||||||
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
|
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
|
||||||
Description = "Badge without a url.",
|
Description = "Badge without a url.",
|
||||||
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.jpg",
|
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor@2x.png",
|
||||||
|
ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor.png",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Title = "osu!volunteer",
|
Title = "osu!volunteer",
|
||||||
|
@ -5,20 +5,29 @@ using System;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Profile.Header.Components;
|
using osu.Game.Overlays.Profile.Header.Components;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
namespace osu.Game.Tests.Visual.Online
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public partial class TestSceneUserProfilePreviousUsernames : OsuTestScene
|
public partial class TestSceneUserProfilePreviousUsernamesDisplay : OsuTestScene
|
||||||
{
|
{
|
||||||
private PreviousUsernames container = null!;
|
private PreviousUsernamesDisplay container = null!;
|
||||||
|
private OverlayColourProvider colourProvider = null!;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp() => Schedule(() =>
|
public void SetUp() => Schedule(() =>
|
||||||
{
|
{
|
||||||
Child = container = new PreviousUsernames
|
colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
|
||||||
|
Child = new DependencyProvidingContainer
|
||||||
{
|
{
|
||||||
|
Child = container = new PreviousUsernamesDisplay
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
},
|
||||||
|
CachedDependencies = new (Type, object)[] { (typeof(OverlayColourProvider), colourProvider) },
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
};
|
};
|
@ -39,6 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
private BeatmapInfo currentSelection => carousel.SelectedBeatmapInfo;
|
private BeatmapInfo currentSelection => carousel.SelectedBeatmapInfo;
|
||||||
|
|
||||||
private const int set_count = 5;
|
private const int set_count = 5;
|
||||||
|
private const int diff_count = 3;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(RulesetStore rulesets)
|
private void load(RulesetStore rulesets)
|
||||||
@ -111,7 +112,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestScrollPositionMaintainedOnAdd()
|
public void TestScrollPositionMaintainedOnAdd()
|
||||||
{
|
{
|
||||||
loadBeatmaps(count: 1, randomDifficulties: false);
|
loadBeatmaps(setCount: 1);
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
@ -124,7 +125,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestDeletion()
|
public void TestDeletion()
|
||||||
{
|
{
|
||||||
loadBeatmaps(count: 5, randomDifficulties: true);
|
loadBeatmaps(setCount: 5, randomDifficulties: true);
|
||||||
|
|
||||||
AddStep("remove first set", () => carousel.RemoveBeatmapSet(carousel.Items.Select(item => item.Item).OfType<CarouselBeatmapSet>().First().BeatmapSet));
|
AddStep("remove first set", () => carousel.RemoveBeatmapSet(carousel.Items.Select(item => item.Item).OfType<CarouselBeatmapSet>().First().BeatmapSet));
|
||||||
AddUntilStep("4 beatmap sets visible", () => this.ChildrenOfType<DrawableCarouselBeatmapSet>().Count(set => set.Alpha > 0) == 4);
|
AddUntilStep("4 beatmap sets visible", () => this.ChildrenOfType<DrawableCarouselBeatmapSet>().Count(set => set.Alpha > 0) == 4);
|
||||||
@ -133,7 +134,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestScrollPositionMaintainedOnDelete()
|
public void TestScrollPositionMaintainedOnDelete()
|
||||||
{
|
{
|
||||||
loadBeatmaps(count: 50, randomDifficulties: false);
|
loadBeatmaps(setCount: 50);
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
@ -150,7 +151,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestManyPanels()
|
public void TestManyPanels()
|
||||||
{
|
{
|
||||||
loadBeatmaps(count: 5000, randomDifficulties: true);
|
loadBeatmaps(setCount: 5000, randomDifficulties: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -501,6 +502,34 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
waitForSelection(set_count);
|
waitForSelection(set_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddRemoveDifficultySort()
|
||||||
|
{
|
||||||
|
const int local_set_count = 2;
|
||||||
|
const int local_diff_count = 2;
|
||||||
|
|
||||||
|
loadBeatmaps(setCount: local_set_count, diffCount: local_diff_count);
|
||||||
|
|
||||||
|
AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false));
|
||||||
|
|
||||||
|
checkVisibleItemCount(false, local_set_count * local_diff_count);
|
||||||
|
|
||||||
|
var firstAdded = TestResources.CreateTestBeatmapSetInfo(local_diff_count);
|
||||||
|
firstAdded.Status = BeatmapOnlineStatus.Loved;
|
||||||
|
|
||||||
|
AddStep("Add new set", () => carousel.UpdateBeatmapSet(firstAdded));
|
||||||
|
|
||||||
|
checkVisibleItemCount(false, (local_set_count + 1) * local_diff_count);
|
||||||
|
|
||||||
|
AddStep("Remove set", () => carousel.RemoveBeatmapSet(firstAdded));
|
||||||
|
|
||||||
|
checkVisibleItemCount(false, (local_set_count) * local_diff_count);
|
||||||
|
|
||||||
|
setSelected(local_set_count, 1);
|
||||||
|
|
||||||
|
waitForSelection(local_set_count);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSelectionEnteringFromEmptyRuleset()
|
public void TestSelectionEnteringFromEmptyRuleset()
|
||||||
{
|
{
|
||||||
@ -662,7 +691,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < 3; i++)
|
||||||
{
|
{
|
||||||
var set = TestResources.CreateTestBeatmapSetInfo(3);
|
var set = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
|
|
||||||
// only need to set the first as they are a shared reference.
|
// only need to set the first as they are a shared reference.
|
||||||
var beatmap = set.Beatmaps.First();
|
var beatmap = set.Beatmaps.First();
|
||||||
@ -709,7 +738,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < 3; i++)
|
||||||
{
|
{
|
||||||
var set = TestResources.CreateTestBeatmapSetInfo(3);
|
var set = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
|
|
||||||
// only need to set the first as they are a shared reference.
|
// only need to set the first as they are a shared reference.
|
||||||
var beatmap = set.Beatmaps.First();
|
var beatmap = set.Beatmaps.First();
|
||||||
@ -758,32 +787,54 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSortingWithFiltered()
|
public void TestSortingWithDifficultyFiltered()
|
||||||
{
|
{
|
||||||
|
const int local_diff_count = 3;
|
||||||
|
const int local_set_count = 2;
|
||||||
|
|
||||||
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
|
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
AddStep("Populuate beatmap sets", () =>
|
AddStep("Populuate beatmap sets", () =>
|
||||||
{
|
{
|
||||||
sets.Clear();
|
sets.Clear();
|
||||||
|
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < local_set_count; i++)
|
||||||
{
|
{
|
||||||
var set = TestResources.CreateTestBeatmapSetInfo(3);
|
var set = TestResources.CreateTestBeatmapSetInfo(local_diff_count);
|
||||||
set.Beatmaps[0].StarRating = 3 - i;
|
set.Beatmaps[0].StarRating = 3 - i;
|
||||||
set.Beatmaps[2].StarRating = 6 + i;
|
set.Beatmaps[1].StarRating = 6 + i;
|
||||||
sets.Add(set);
|
sets.Add(set);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
loadBeatmaps(sets);
|
loadBeatmaps(sets);
|
||||||
|
|
||||||
|
AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false));
|
||||||
|
|
||||||
|
checkVisibleItemCount(false, local_set_count * local_diff_count);
|
||||||
|
checkVisibleItemCount(true, 1);
|
||||||
|
|
||||||
AddStep("Filter to normal", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Normal" }, false));
|
AddStep("Filter to normal", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Normal" }, false));
|
||||||
AddAssert("Check first set at end", () => carousel.BeatmapSets.First().Equals(sets.Last()));
|
checkVisibleItemCount(false, local_set_count);
|
||||||
AddAssert("Check last set at start", () => carousel.BeatmapSets.Last().Equals(sets.First()));
|
checkVisibleItemCount(true, 1);
|
||||||
|
|
||||||
|
AddUntilStep("Check all visible sets have one normal", () =>
|
||||||
|
{
|
||||||
|
return carousel.Items.OfType<DrawableCarouselBeatmapSet>()
|
||||||
|
.Where(p => p.IsPresent)
|
||||||
|
.Count(p => ((CarouselBeatmapSet)p.Item)!.Beatmaps.Single().BeatmapInfo.DifficultyName.StartsWith("Normal", StringComparison.Ordinal)) == local_set_count;
|
||||||
|
});
|
||||||
|
|
||||||
AddStep("Filter to insane", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Insane" }, false));
|
AddStep("Filter to insane", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Insane" }, false));
|
||||||
AddAssert("Check first set at start", () => carousel.BeatmapSets.First().Equals(sets.First()));
|
checkVisibleItemCount(false, local_set_count);
|
||||||
AddAssert("Check last set at end", () => carousel.BeatmapSets.Last().Equals(sets.Last()));
|
checkVisibleItemCount(true, 1);
|
||||||
|
|
||||||
|
AddUntilStep("Check all visible sets have one insane", () =>
|
||||||
|
{
|
||||||
|
return carousel.Items.OfType<DrawableCarouselBeatmapSet>()
|
||||||
|
.Where(p => p.IsPresent)
|
||||||
|
.Count(p => ((CarouselBeatmapSet)p.Item)!.Beatmaps.Single().BeatmapInfo.DifficultyName.StartsWith("Insane", StringComparison.Ordinal)) == local_set_count;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -838,7 +889,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
AddStep("create hidden set", () =>
|
AddStep("create hidden set", () =>
|
||||||
{
|
{
|
||||||
hidingSet = TestResources.CreateTestBeatmapSetInfo(3);
|
hidingSet = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
hidingSet.Beatmaps[1].Hidden = true;
|
hidingSet.Beatmaps[1].Hidden = true;
|
||||||
|
|
||||||
hiddenList.Clear();
|
hiddenList.Clear();
|
||||||
@ -885,7 +936,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
AddStep("add mixed ruleset beatmapset", () =>
|
AddStep("add mixed ruleset beatmapset", () =>
|
||||||
{
|
{
|
||||||
testMixed = TestResources.CreateTestBeatmapSetInfo(3);
|
testMixed = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
|
|
||||||
for (int i = 0; i <= 2; i++)
|
for (int i = 0; i <= 2; i++)
|
||||||
{
|
{
|
||||||
@ -907,7 +958,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
BeatmapSetInfo testSingle = null;
|
BeatmapSetInfo testSingle = null;
|
||||||
AddStep("add single ruleset beatmapset", () =>
|
AddStep("add single ruleset beatmapset", () =>
|
||||||
{
|
{
|
||||||
testSingle = TestResources.CreateTestBeatmapSetInfo(3);
|
testSingle = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
testSingle.Beatmaps.ForEach(b =>
|
testSingle.Beatmaps.ForEach(b =>
|
||||||
{
|
{
|
||||||
b.Ruleset = rulesets.AvailableRulesets.ElementAt(1);
|
b.Ruleset = rulesets.AvailableRulesets.ElementAt(1);
|
||||||
@ -930,7 +981,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
manySets.Clear();
|
manySets.Clear();
|
||||||
|
|
||||||
for (int i = 1; i <= 50; i++)
|
for (int i = 1; i <= 50; i++)
|
||||||
manySets.Add(TestResources.CreateTestBeatmapSetInfo(3));
|
manySets.Add(TestResources.CreateTestBeatmapSetInfo(diff_count));
|
||||||
});
|
});
|
||||||
|
|
||||||
loadBeatmaps(manySets);
|
loadBeatmaps(manySets);
|
||||||
@ -955,6 +1006,43 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1);
|
AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCarouselRemembersSelectionDifficultySort()
|
||||||
|
{
|
||||||
|
List<BeatmapSetInfo> manySets = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
|
AddStep("Populate beatmap sets", () =>
|
||||||
|
{
|
||||||
|
manySets.Clear();
|
||||||
|
|
||||||
|
for (int i = 1; i <= 50; i++)
|
||||||
|
manySets.Add(TestResources.CreateTestBeatmapSetInfo(diff_count));
|
||||||
|
});
|
||||||
|
|
||||||
|
loadBeatmaps(manySets);
|
||||||
|
|
||||||
|
AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false));
|
||||||
|
|
||||||
|
advanceSelection(direction: 1, diff: false);
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
AddStep("Toggle non-matching filter", () =>
|
||||||
|
{
|
||||||
|
carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("Restore no filter", () =>
|
||||||
|
{
|
||||||
|
carousel.Filter(new FilterCriteria(), false);
|
||||||
|
eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// always returns to same selection as long as it's available.
|
||||||
|
AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestFilteringByUserStarDifficulty()
|
public void TestFilteringByUserStarDifficulty()
|
||||||
{
|
{
|
||||||
@ -1081,20 +1169,26 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null, int? count = null,
|
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null,
|
||||||
bool randomDifficulties = false)
|
int? setCount = null, int? diffCount = null, bool randomDifficulties = false)
|
||||||
{
|
{
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
|
|
||||||
if (beatmapSets == null)
|
if (beatmapSets == null)
|
||||||
{
|
{
|
||||||
beatmapSets = new List<BeatmapSetInfo>();
|
beatmapSets = new List<BeatmapSetInfo>();
|
||||||
|
var statuses = Enum.GetValues<BeatmapOnlineStatus>()
|
||||||
|
.Except(new[] { BeatmapOnlineStatus.None }) // make sure a badge is always shown.
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
for (int i = 1; i <= (count ?? set_count); i++)
|
for (int i = 1; i <= (setCount ?? set_count); i++)
|
||||||
{
|
{
|
||||||
beatmapSets.Add(randomDifficulties
|
var set = randomDifficulties
|
||||||
? TestResources.CreateTestBeatmapSetInfo()
|
? TestResources.CreateTestBeatmapSetInfo()
|
||||||
: TestResources.CreateTestBeatmapSetInfo(3));
|
: TestResources.CreateTestBeatmapSetInfo(diffCount ?? diff_count);
|
||||||
|
set.Status = statuses[RNG.Next(statuses.Length)];
|
||||||
|
|
||||||
|
beatmapSets.Add(set);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Resources.Localisation.Web;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Catch;
|
using osu.Game.Rulesets.Catch;
|
||||||
using osu.Game.Rulesets.Mania;
|
using osu.Game.Rulesets.Mania;
|
||||||
@ -188,7 +189,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
{
|
{
|
||||||
AddUntilStep($"displayed bpm is {target}", () =>
|
AddUntilStep($"displayed bpm is {target}", () =>
|
||||||
{
|
{
|
||||||
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Single(l => l.Statistic.Name == "BPM");
|
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsBpm);
|
||||||
return label.Statistic.Content == target;
|
return label.Statistic.Content == target;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
219
osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs
Normal file
219
osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Screens.Select;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.SongSelect
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public partial class TestSceneBeatmapInfoWedgeV2 : OsuTestScene
|
||||||
|
{
|
||||||
|
private RulesetStore rulesets = null!;
|
||||||
|
private TestBeatmapInfoWedgeV2 infoWedge = null!;
|
||||||
|
private readonly List<IBeatmap> beatmaps = new List<IBeatmap>();
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(RulesetStore rulesets)
|
||||||
|
{
|
||||||
|
this.rulesets = rulesets;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
AddRange(new Drawable[]
|
||||||
|
{
|
||||||
|
// This exists only to make the wedge more visible in the test scene
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Y = -20,
|
||||||
|
Colour = Colour4.Cornsilk.Darken(0.2f),
|
||||||
|
Height = BeatmapInfoWedgeV2.WEDGE_HEIGHT + 40,
|
||||||
|
Width = 0.65f,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Margin = new MarginPadding { Top = 20, Left = -10 }
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Padding = new MarginPadding { Top = 20 },
|
||||||
|
Child = infoWedge = new TestBeatmapInfoWedgeV2
|
||||||
|
{
|
||||||
|
Width = 0.6f,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AddSliderStep("change star difficulty", 0, 11.9, 5.55, v =>
|
||||||
|
{
|
||||||
|
foreach (var hasCurrentValue in infoWedge.ChildrenOfType<IHasCurrentValue<StarDifficulty>>())
|
||||||
|
hasCurrentValue.Current.Value = new StarDifficulty(v, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRulesetChange()
|
||||||
|
{
|
||||||
|
selectBeatmap(Beatmap.Value.Beatmap);
|
||||||
|
|
||||||
|
AddWaitStep("wait for select", 3);
|
||||||
|
|
||||||
|
foreach (var rulesetInfo in rulesets.AvailableRulesets)
|
||||||
|
{
|
||||||
|
var instance = rulesetInfo.CreateInstance();
|
||||||
|
var testBeatmap = createTestBeatmap(rulesetInfo);
|
||||||
|
|
||||||
|
beatmaps.Add(testBeatmap);
|
||||||
|
|
||||||
|
setRuleset(rulesetInfo);
|
||||||
|
|
||||||
|
selectBeatmap(testBeatmap);
|
||||||
|
|
||||||
|
testBeatmapLabels(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestWedgeVisibility()
|
||||||
|
{
|
||||||
|
AddStep("hide", () => { infoWedge.Hide(); });
|
||||||
|
AddWaitStep("wait for hide", 3);
|
||||||
|
AddAssert("check visibility", () => infoWedge.Alpha == 0);
|
||||||
|
AddStep("show", () => { infoWedge.Show(); });
|
||||||
|
AddWaitStep("wait for show", 1);
|
||||||
|
AddAssert("check visibility", () => infoWedge.Alpha > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testBeatmapLabels(Ruleset ruleset)
|
||||||
|
{
|
||||||
|
AddAssert("check title", () => infoWedge.Info!.TitleLabel.Current.Value == $"{ruleset.ShortName}Title");
|
||||||
|
AddAssert("check artist", () => infoWedge.Info!.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist");
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("reset mods", () => SelectedMods.SetDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTruncation()
|
||||||
|
{
|
||||||
|
selectBeatmap(createLongMetadata());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNullBeatmapWithBackground()
|
||||||
|
{
|
||||||
|
selectBeatmap(null);
|
||||||
|
AddAssert("check default title", () => infoWedge.Info!.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title);
|
||||||
|
AddAssert("check default artist", () => infoWedge.Info!.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist);
|
||||||
|
AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setRuleset(RulesetInfo rulesetInfo)
|
||||||
|
{
|
||||||
|
Container? containerBefore = null;
|
||||||
|
|
||||||
|
AddStep("set ruleset", () =>
|
||||||
|
{
|
||||||
|
// wedge content is only refreshed if the ruleset changes, so only wait for load in that case.
|
||||||
|
if (!rulesetInfo.Equals(Ruleset.Value))
|
||||||
|
containerBefore = infoWedge.DisplayedContent;
|
||||||
|
|
||||||
|
Ruleset.Value = rulesetInfo;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectBeatmap(IBeatmap? b)
|
||||||
|
{
|
||||||
|
Container? containerBefore = null;
|
||||||
|
|
||||||
|
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
|
||||||
|
{
|
||||||
|
containerBefore = infoWedge.DisplayedContent;
|
||||||
|
infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b);
|
||||||
|
infoWedge.Show();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IBeatmap createTestBeatmap(RulesetInfo ruleset)
|
||||||
|
{
|
||||||
|
List<HitObject> objects = new List<HitObject>();
|
||||||
|
for (double i = 0; i < 50000; i += 1000)
|
||||||
|
objects.Add(new TestHitObject { StartTime = i });
|
||||||
|
|
||||||
|
return new Beatmap
|
||||||
|
{
|
||||||
|
BeatmapInfo = new BeatmapInfo
|
||||||
|
{
|
||||||
|
Metadata = new BeatmapMetadata
|
||||||
|
{
|
||||||
|
Author = { Username = $"{ruleset.ShortName}Author" },
|
||||||
|
Artist = $"{ruleset.ShortName}Artist",
|
||||||
|
Source = $"{ruleset.ShortName}Source",
|
||||||
|
Title = $"{ruleset.ShortName}Title"
|
||||||
|
},
|
||||||
|
Ruleset = ruleset,
|
||||||
|
StarRating = 6,
|
||||||
|
DifficultyName = $"{ruleset.ShortName}Version",
|
||||||
|
Difficulty = new BeatmapDifficulty()
|
||||||
|
},
|
||||||
|
HitObjects = objects
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private IBeatmap createLongMetadata()
|
||||||
|
{
|
||||||
|
return new Beatmap
|
||||||
|
{
|
||||||
|
BeatmapInfo = new BeatmapInfo
|
||||||
|
{
|
||||||
|
Metadata = new BeatmapMetadata
|
||||||
|
{
|
||||||
|
Author = { Username = "WWWWWWWWWWWWWWW" },
|
||||||
|
Artist = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Artist",
|
||||||
|
Source = "Verrrrry long Source",
|
||||||
|
Title = "Verrrrry long Title"
|
||||||
|
},
|
||||||
|
DifficultyName = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version",
|
||||||
|
Status = BeatmapOnlineStatus.Graveyard,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class TestBeatmapInfoWedgeV2 : BeatmapInfoWedgeV2
|
||||||
|
{
|
||||||
|
public new Container? DisplayedContent => base.DisplayedContent;
|
||||||
|
public new WedgeInfoText? Info => base.Info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestHitObject : ConvertHitObject, IHasPosition
|
||||||
|
{
|
||||||
|
public float X => 0;
|
||||||
|
public float Y => 0;
|
||||||
|
public Vector2 Position { get; } = Vector2.Zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,9 +6,11 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Mods;
|
using osu.Game.Overlays.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.Select.FooterV2;
|
using osu.Game.Screens.Select.FooterV2;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
@ -37,10 +39,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
footer = new FooterV2
|
new PopoverContainer
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Origin = Anchor.Centre
|
Child = footer = new FooterV2(),
|
||||||
},
|
},
|
||||||
overlay = new DummyOverlay()
|
overlay = new DummyOverlay()
|
||||||
};
|
};
|
||||||
@ -56,6 +58,24 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
overlay.Hide();
|
overlay.Hide();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestShowOptions()
|
||||||
|
{
|
||||||
|
AddStep("enable options", () =>
|
||||||
|
{
|
||||||
|
var optionsButton = this.ChildrenOfType<FooterButtonV2>().Last();
|
||||||
|
|
||||||
|
optionsButton.Enabled.Value = true;
|
||||||
|
optionsButton.TriggerClick();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestState()
|
public void TestState()
|
||||||
{
|
{
|
||||||
|
@ -15,6 +15,7 @@ using osu.Game.Overlays;
|
|||||||
using osu.Game.Overlays.Dialog;
|
using osu.Game.Overlays.Dialog;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
using osu.Game.Screens.Select.Carousel;
|
using osu.Game.Screens.Select.Carousel;
|
||||||
|
using osu.Game.Screens.Select.Filter;
|
||||||
using osu.Game.Tests.Online;
|
using osu.Game.Tests.Online;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
@ -192,6 +193,57 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
|
AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSplitDisplay()
|
||||||
|
{
|
||||||
|
ArchiveDownloadRequest<IBeatmapSetInfo>? downloadRequest = null;
|
||||||
|
|
||||||
|
AddStep("set difficulty sort mode", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }));
|
||||||
|
AddStep("update online hash", () =>
|
||||||
|
{
|
||||||
|
testBeatmapSetInfo.Beatmaps.First().OnlineMD5Hash = "different hash";
|
||||||
|
testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
|
||||||
|
|
||||||
|
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("multiple \"sets\" visible", () => carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().Count(), () => Is.GreaterThan(1));
|
||||||
|
AddUntilStep("update button visible", getUpdateButton, () => Is.Not.Null);
|
||||||
|
|
||||||
|
AddStep("click button", () => getUpdateButton()?.TriggerClick());
|
||||||
|
|
||||||
|
AddUntilStep("wait for download started", () =>
|
||||||
|
{
|
||||||
|
downloadRequest = beatmapDownloader.GetExistingDownload(testBeatmapSetInfo);
|
||||||
|
return downloadRequest != null;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for button disabled", () => getUpdateButton()?.Enabled.Value == false);
|
||||||
|
|
||||||
|
AddUntilStep("progress download to completion", () =>
|
||||||
|
{
|
||||||
|
if (downloadRequest is TestSceneOnlinePlayBeatmapAvailabilityTracker.TestDownloadRequest testRequest)
|
||||||
|
{
|
||||||
|
testRequest.SetProgress(testRequest.Progress + 0.1f);
|
||||||
|
|
||||||
|
if (testRequest.Progress >= 1)
|
||||||
|
{
|
||||||
|
testRequest.TriggerSuccess();
|
||||||
|
|
||||||
|
// usually this would be done by the import process.
|
||||||
|
testBeatmapSetInfo.Beatmaps.First().MD5Hash = "different hash";
|
||||||
|
testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
|
||||||
|
|
||||||
|
// usually this would be done by a realm subscription.
|
||||||
|
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private BeatmapCarousel createCarousel()
|
private BeatmapCarousel createCarousel()
|
||||||
{
|
{
|
||||||
return carousel = new BeatmapCarousel
|
return carousel = new BeatmapCarousel
|
||||||
@ -199,7 +251,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
BeatmapSets = new List<BeatmapSetInfo>
|
BeatmapSets = new List<BeatmapSetInfo>
|
||||||
{
|
{
|
||||||
(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo()),
|
(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(5)),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Tests.Scores.IO;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public partial class TestSceneMissingBeatmapNotification : OsuTestScene
|
||||||
|
{
|
||||||
|
[Cached]
|
||||||
|
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Child = new Container
|
||||||
|
{
|
||||||
|
Width = 280,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Child = new MissingBeatmapNotification(CreateAPIBeatmapSet(Ruleset.Value).Beatmaps.First(), new ImportScoreTest.TestArchiveReader(), "deadbeef")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,131 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Mods;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public partial class TestSceneModEffectPreviewPanel : OsuTestScene
|
||||||
|
{
|
||||||
|
[Cached(typeof(BeatmapDifficultyCache))]
|
||||||
|
private TestBeatmapDifficultyCache difficultyCache = new TestBeatmapDifficultyCache();
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||||
|
|
||||||
|
private Container content = null!;
|
||||||
|
protected override Container<Drawable> Content => content;
|
||||||
|
|
||||||
|
private BeatmapAttributesDisplay panel = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
base.Content.AddRange(new Drawable[]
|
||||||
|
{
|
||||||
|
difficultyCache,
|
||||||
|
content = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDisplay()
|
||||||
|
{
|
||||||
|
OsuModDifficultyAdjust difficultyAdjust = new OsuModDifficultyAdjust();
|
||||||
|
OsuModDoubleTime doubleTime = new OsuModDoubleTime();
|
||||||
|
|
||||||
|
AddStep("create display", () => Child = panel = new BeatmapAttributesDisplay
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("set beatmap", () =>
|
||||||
|
{
|
||||||
|
var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo)
|
||||||
|
{
|
||||||
|
BeatmapInfo =
|
||||||
|
{
|
||||||
|
BPM = 120
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ruleset.Value = beatmap.BeatmapInfo.Ruleset;
|
||||||
|
panel.BeatmapInfo.Value = beatmap.BeatmapInfo;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddSliderStep("change star rating", 0, 10d, 5, stars =>
|
||||||
|
{
|
||||||
|
if (panel.IsNotNull())
|
||||||
|
previewStarRating(stars);
|
||||||
|
});
|
||||||
|
AddStep("preview ridiculously high SR", () => previewStarRating(1234));
|
||||||
|
|
||||||
|
AddStep("add DA to mods", () => SelectedMods.Value = new[] { difficultyAdjust });
|
||||||
|
|
||||||
|
AddSliderStep("change AR", 0, 10f, 5, ar =>
|
||||||
|
{
|
||||||
|
if (panel.IsNotNull())
|
||||||
|
difficultyAdjust.ApproachRate.Value = ar;
|
||||||
|
});
|
||||||
|
AddSliderStep("change CS", 0, 10f, 5, cs =>
|
||||||
|
{
|
||||||
|
if (panel.IsNotNull())
|
||||||
|
difficultyAdjust.CircleSize.Value = cs;
|
||||||
|
});
|
||||||
|
AddSliderStep("change HP", 0, 10f, 5, hp =>
|
||||||
|
{
|
||||||
|
if (panel.IsNotNull())
|
||||||
|
difficultyAdjust.DrainRate.Value = hp;
|
||||||
|
});
|
||||||
|
AddSliderStep("change OD", 0, 10f, 5, od =>
|
||||||
|
{
|
||||||
|
if (panel.IsNotNull())
|
||||||
|
difficultyAdjust.OverallDifficulty.Value = od;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("add DT to mods", () => SelectedMods.Value = new Mod[] { difficultyAdjust, doubleTime });
|
||||||
|
AddSliderStep("change rate", 1.01d, 2d, 1.5d, rate =>
|
||||||
|
{
|
||||||
|
if (panel.IsNotNull())
|
||||||
|
doubleTime.SpeedChange.Value = rate;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddToggleStep("toggle collapsed", collapsed => panel.Collapsed.Value = collapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void previewStarRating(double stars)
|
||||||
|
{
|
||||||
|
difficultyCache.Difficulty = new StarDifficulty(stars, 0);
|
||||||
|
panel.BeatmapInfo.TriggerChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache
|
||||||
|
{
|
||||||
|
public StarDifficulty? Difficulty { get; set; }
|
||||||
|
|
||||||
|
public override Task<StarDifficulty?> GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable<Mod>? mods = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(Difficulty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -51,6 +51,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
AddStep("clear contents", Clear);
|
AddStep("clear contents", Clear);
|
||||||
AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0));
|
AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0));
|
||||||
AddStep("reset mods", () => SelectedMods.SetDefault());
|
AddStep("reset mods", () => SelectedMods.SetDefault());
|
||||||
|
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo));
|
||||||
AddStep("set up presets", () =>
|
AddStep("set up presets", () =>
|
||||||
{
|
{
|
||||||
Realm.Write(r =>
|
Realm.Write(r =>
|
||||||
@ -92,6 +93,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
State = { Value = Visibility.Visible },
|
State = { Value = Visibility.Visible },
|
||||||
|
Beatmap = Beatmap.Value,
|
||||||
SelectedMods = { BindTarget = SelectedMods }
|
SelectedMods = { BindTarget = SelectedMods }
|
||||||
});
|
});
|
||||||
waitForColumnLoad();
|
waitForColumnLoad();
|
||||||
@ -113,7 +115,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
AddAssert("mod multiplier correct", () =>
|
AddAssert("mod multiplier correct", () =>
|
||||||
{
|
{
|
||||||
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
|
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
|
||||||
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value);
|
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value);
|
||||||
});
|
});
|
||||||
assertCustomisationToggleState(disabled: false, active: false);
|
assertCustomisationToggleState(disabled: false, active: false);
|
||||||
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
|
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
|
||||||
@ -128,7 +130,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
AddAssert("mod multiplier correct", () =>
|
AddAssert("mod multiplier correct", () =>
|
||||||
{
|
{
|
||||||
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
|
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
|
||||||
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value);
|
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value);
|
||||||
});
|
});
|
||||||
assertCustomisationToggleState(disabled: false, active: false);
|
assertCustomisationToggleState(disabled: false, active: false);
|
||||||
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
|
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
|
||||||
@ -785,7 +787,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
InputManager.MoveMouseTo(this.ChildrenOfType<ModPresetPanel>().Single(preset => preset.Preset.Value.Name == "Half Time 0.5x"));
|
InputManager.MoveMouseTo(this.ChildrenOfType<ModPresetPanel>().Single(preset => preset.Preset.Value.Name == "Half Time 0.5x"));
|
||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
AddAssert("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.5));
|
AddAssert("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.5));
|
||||||
|
|
||||||
// this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation,
|
// this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation,
|
||||||
// it is instrumental in the reproduction of the failure scenario that this test is supposed to cover.
|
// it is instrumental in the reproduction of the failure scenario that this test is supposed to cover.
|
||||||
@ -794,7 +796,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick());
|
AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick());
|
||||||
AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType<ModSettingsArea>().Single()
|
AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType<ModSettingsArea>().Single()
|
||||||
.ChildrenOfType<RevertToDefaultButton<double>>().Single().TriggerClick());
|
.ChildrenOfType<RevertToDefaultButton<double>>().Single().TriggerClick());
|
||||||
AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.7));
|
AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.7));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void waitForColumnLoad() => AddUntilStep("all column content loaded",
|
private void waitForColumnLoad() => AddUntilStep("all column content loaded",
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System.Linq;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Graphics.Shapes;
|
|
||||||
using osu.Framework.Localisation;
|
|
||||||
using osu.Framework.Testing;
|
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osu.Game.Overlays;
|
|
||||||
using osu.Game.Overlays.Mods;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osuTK.Graphics;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.UserInterface
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public partial class TestSceneModsEffectDisplay : OsuTestScene
|
|
||||||
{
|
|
||||||
[Cached]
|
|
||||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private OsuColour colours { get; set; } = null!;
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestModsEffectDisplay()
|
|
||||||
{
|
|
||||||
TestDisplay testDisplay = null!;
|
|
||||||
Box background = null!;
|
|
||||||
|
|
||||||
AddStep("add display", () =>
|
|
||||||
{
|
|
||||||
Add(testDisplay = new TestDisplay
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre
|
|
||||||
});
|
|
||||||
var boxes = testDisplay.ChildrenOfType<Box>();
|
|
||||||
background = boxes.First();
|
|
||||||
});
|
|
||||||
|
|
||||||
AddStep("set value to default", () => testDisplay.Current.Value = 50);
|
|
||||||
AddUntilStep("colours are correct", () => testDisplay.Container.Colour == Color4.White && background.Colour == colourProvider.Background3);
|
|
||||||
|
|
||||||
AddStep("set value to less", () => testDisplay.Current.Value = 40);
|
|
||||||
AddUntilStep("colours are correct", () => testDisplay.Container.Colour == colourProvider.Background5 && background.Colour == colours.ForModType(ModType.DifficultyReduction));
|
|
||||||
|
|
||||||
AddStep("set value to bigger", () => testDisplay.Current.Value = 60);
|
|
||||||
AddUntilStep("colours are correct", () => testDisplay.Container.Colour == colourProvider.Background5 && background.Colour == colours.ForModType(ModType.DifficultyIncrease));
|
|
||||||
}
|
|
||||||
|
|
||||||
private partial class TestDisplay : ModsEffectDisplay
|
|
||||||
{
|
|
||||||
public Container<Drawable> Container => Content;
|
|
||||||
|
|
||||||
protected override LocalisableString Label => "Test display";
|
|
||||||
|
|
||||||
public TestDisplay()
|
|
||||||
{
|
|
||||||
Current.Default = 50;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,9 @@
|
|||||||
// 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 NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Mods;
|
using osu.Game.Overlays.Mods;
|
||||||
@ -12,17 +11,17 @@ using osu.Game.Overlays.Mods;
|
|||||||
namespace osu.Game.Tests.Visual.UserInterface
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public partial class TestSceneDifficultyMultiplierDisplay : OsuTestScene
|
public partial class TestSceneScoreMultiplierDisplay : OsuTestScene
|
||||||
{
|
{
|
||||||
[Cached]
|
[Cached]
|
||||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDifficultyMultiplierDisplay()
|
public void TestBasic()
|
||||||
{
|
{
|
||||||
DifficultyMultiplierDisplay multiplierDisplay = null;
|
ScoreMultiplierDisplay multiplierDisplay = null!;
|
||||||
|
|
||||||
AddStep("create content", () => Child = multiplierDisplay = new DifficultyMultiplierDisplay
|
AddStep("create content", () => Child = multiplierDisplay = new ScoreMultiplierDisplay
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre
|
Origin = Anchor.Centre
|
||||||
@ -34,7 +33,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
|
|
||||||
AddSliderStep("set multiplier", 0, 2, 1d, multiplier =>
|
AddSliderStep("set multiplier", 0, 2, 1d, multiplier =>
|
||||||
{
|
{
|
||||||
if (multiplierDisplay != null)
|
if (multiplierDisplay.IsNotNull())
|
||||||
multiplierDisplay.Current.Value = multiplier;
|
multiplierDisplay.Current.Value = multiplier;
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -2,10 +2,10 @@
|
|||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="DeepEqual" Version="4.2.1" />
|
<PackageReference Include="DeepEqual" Version="4.2.1" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||||
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
<PackageReference Include="Nito.AsyncEx" Version="5.1.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" />
|
||||||
<PackageReference Include="Moq" Version="4.18.4" />
|
<PackageReference Include="Moq" Version="4.18.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
|
@ -58,9 +58,14 @@ namespace osu.Game.Tournament.Tests.Components
|
|||||||
|
|
||||||
songBar.Beatmap = new TournamentBeatmap(beatmap);
|
songBar.Beatmap = new TournamentBeatmap(beatmap);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("set mods to HR", () => songBar.Mods = LegacyMods.HardRock);
|
AddStep("set mods to HR", () => songBar.Mods = LegacyMods.HardRock);
|
||||||
AddStep("set mods to DT", () => songBar.Mods = LegacyMods.DoubleTime);
|
AddStep("set mods to DT", () => songBar.Mods = LegacyMods.DoubleTime);
|
||||||
AddStep("unset mods", () => songBar.Mods = LegacyMods.None);
|
AddStep("unset mods", () => songBar.Mods = LegacyMods.None);
|
||||||
|
|
||||||
|
AddToggleStep("toggle expanded", expanded => songBar.Expanded = expanded);
|
||||||
|
|
||||||
|
AddStep("set null beatmap", () => songBar.Beatmap = null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,13 @@ namespace osu.Game.Tournament.Tests.Screens
|
|||||||
{
|
{
|
||||||
public partial class TestSceneScheduleScreen : TournamentScreenTestScene
|
public partial class TestSceneScheduleScreen : TournamentScreenTestScene
|
||||||
{
|
{
|
||||||
|
public override void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("clear matches", () => Ladder.Matches.Clear());
|
||||||
|
|
||||||
|
base.SetUpSteps();
|
||||||
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
@ -34,6 +41,36 @@ namespace osu.Game.Tournament.Tests.Screens
|
|||||||
AddStep("Set null current match", () => Ladder.CurrentMatch.Value = null);
|
AddStep("Set null current match", () => Ladder.CurrentMatch.Value = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUpcomingMatches()
|
||||||
|
{
|
||||||
|
AddStep("Add upcoming match", () =>
|
||||||
|
{
|
||||||
|
var tournamentMatch = CreateSampleMatch();
|
||||||
|
|
||||||
|
tournamentMatch.Date.Value = DateTimeOffset.UtcNow.AddMinutes(5);
|
||||||
|
tournamentMatch.Completed.Value = false;
|
||||||
|
|
||||||
|
Ladder.Matches.Add(tournamentMatch);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRecentMatches()
|
||||||
|
{
|
||||||
|
AddStep("Add recent match", () =>
|
||||||
|
{
|
||||||
|
var tournamentMatch = CreateSampleMatch();
|
||||||
|
|
||||||
|
tournamentMatch.Date.Value = DateTimeOffset.UtcNow;
|
||||||
|
tournamentMatch.Completed.Value = true;
|
||||||
|
tournamentMatch.Team1Score.Value = tournamentMatch.PointsToWin;
|
||||||
|
tournamentMatch.Team2Score.Value = tournamentMatch.PointsToWin / 2;
|
||||||
|
|
||||||
|
Ladder.Matches.Add(tournamentMatch);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void setMatchDate(TimeSpan relativeTime)
|
private void setMatchDate(TimeSpan relativeTime)
|
||||||
// Humanizer cannot handle negative timespans.
|
// Humanizer cannot handle negative timespans.
|
||||||
=> AddStep($"start time is {relativeTime}", () =>
|
=> AddStep($"start time is {relativeTime}", () =>
|
||||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Tournament.Tests
|
|||||||
{
|
{
|
||||||
public TournamentScalingContainer()
|
public TournamentScalingContainer()
|
||||||
{
|
{
|
||||||
TargetDrawSize = new Vector2(1920, 1080);
|
TargetDrawSize = new Vector2(1024, 768);
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
|
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
|
||||||
</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>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
@ -92,8 +92,16 @@ namespace osu.Game.Tournament.IPC
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
beatmapLookupRequest = new GetBeatmapRequest(new APIBeatmap { OnlineID = beatmapId });
|
beatmapLookupRequest = new GetBeatmapRequest(new APIBeatmap { OnlineID = beatmapId });
|
||||||
beatmapLookupRequest.Success += b => Beatmap.Value = new TournamentBeatmap(b);
|
beatmapLookupRequest.Success += b =>
|
||||||
beatmapLookupRequest.Failure += _ => Beatmap.Value = null;
|
{
|
||||||
|
if (lastBeatmapId == beatmapId)
|
||||||
|
Beatmap.Value = new TournamentBeatmap(b);
|
||||||
|
};
|
||||||
|
beatmapLookupRequest.Failure += _ =>
|
||||||
|
{
|
||||||
|
if (lastBeatmapId == beatmapId)
|
||||||
|
Beatmap.Value = null;
|
||||||
|
};
|
||||||
API.Queue(beatmapLookupRequest);
|
API.Queue(beatmapLookupRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,16 @@ using osu.Framework.Extensions.Color4Extensions;
|
|||||||
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.Input;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Tournament
|
namespace osu.Game.Tournament
|
||||||
{
|
{
|
||||||
internal partial class SaveChangesOverlay : CompositeDrawable
|
internal partial class SaveChangesOverlay : CompositeDrawable, IKeyBindingHandler<PlatformAction>
|
||||||
{
|
{
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private TournamentGame tournamentGame { get; set; } = null!;
|
private TournamentGame tournamentGame { get; set; } = null!;
|
||||||
@ -78,6 +81,21 @@ namespace osu.Game.Tournament
|
|||||||
scheduleNextCheck();
|
scheduleNextCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
|
||||||
|
{
|
||||||
|
if (e.Action == PlatformAction.Save && !e.Repeat)
|
||||||
|
{
|
||||||
|
saveChangesButton.TriggerClick();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
private void scheduleNextCheck() => Scheduler.AddDelayed(() => checkForChanges().FireAndForget(), 1000);
|
private void scheduleNextCheck() => Scheduler.AddDelayed(() => checkForChanges().FireAndForget(), 1000);
|
||||||
|
|
||||||
private void saveChanges()
|
private void saveChanges()
|
||||||
|
@ -126,7 +126,7 @@ namespace osu.Game.Tournament.Screens.MapPool
|
|||||||
if (CurrentMatch.Value == null || CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) < 2)
|
if (CurrentMatch.Value == null || CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) < 2)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// if bans have already been placed, beatmap changes result in a selection being made autoamtically
|
// if bans have already been placed, beatmap changes result in a selection being made automatically
|
||||||
if (beatmap.NewValue?.OnlineID > 0)
|
if (beatmap.NewValue?.OnlineID > 0)
|
||||||
addForBeatmap(beatmap.NewValue.OnlineID);
|
addForBeatmap(beatmap.NewValue.OnlineID);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// 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.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -19,6 +20,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
{
|
{
|
||||||
public partial class ScheduleScreen : TournamentScreen
|
public partial class ScheduleScreen : TournamentScreen
|
||||||
{
|
{
|
||||||
|
private readonly BindableList<TournamentMatch> allMatches = new BindableList<TournamentMatch>();
|
||||||
private readonly Bindable<TournamentMatch?> currentMatch = new Bindable<TournamentMatch?>();
|
private readonly Bindable<TournamentMatch?> currentMatch = new Bindable<TournamentMatch?>();
|
||||||
private Container mainContainer = null!;
|
private Container mainContainer = null!;
|
||||||
private LadderInfo ladder = null!;
|
private LadderInfo ladder = null!;
|
||||||
@ -101,19 +103,34 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
|
allMatches.BindTo(ladder.Matches);
|
||||||
|
allMatches.BindCollectionChanged((_, _) => refresh());
|
||||||
|
|
||||||
currentMatch.BindTo(ladder.CurrentMatch);
|
currentMatch.BindTo(ladder.CurrentMatch);
|
||||||
currentMatch.BindValueChanged(matchChanged, true);
|
currentMatch.BindValueChanged(_ => refresh(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void matchChanged(ValueChangedEvent<TournamentMatch?> match)
|
private void refresh()
|
||||||
{
|
{
|
||||||
var upcoming = ladder.Matches.Where(p => !p.Completed.Value && p.Team1.Value != null && p.Team2.Value != null && Math.Abs(p.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < 4);
|
const int days_for_displays = 4;
|
||||||
var conditionals = ladder
|
|
||||||
.Matches.Where(p => !p.Completed.Value && (p.Team1.Value == null || p.Team2.Value == null) && Math.Abs(p.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < 4)
|
IEnumerable<ConditionalTournamentMatch> conditionals =
|
||||||
|
allMatches
|
||||||
|
.Where(m => !m.Completed.Value && (m.Team1.Value == null || m.Team2.Value == null) && Math.Abs(m.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < days_for_displays)
|
||||||
.SelectMany(m => m.ConditionalMatches.Where(cp => m.Acronyms.TrueForAll(a => cp.Acronyms.Contains(a))));
|
.SelectMany(m => m.ConditionalMatches.Where(cp => m.Acronyms.TrueForAll(a => cp.Acronyms.Contains(a))));
|
||||||
|
|
||||||
upcoming = upcoming.Concat(conditionals);
|
IEnumerable<TournamentMatch> upcoming =
|
||||||
upcoming = upcoming.OrderBy(p => p.Date.Value).Take(8);
|
allMatches
|
||||||
|
.Where(m => !m.Completed.Value && m.Team1.Value != null && m.Team2.Value != null && Math.Abs(m.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < days_for_displays)
|
||||||
|
.Concat(conditionals)
|
||||||
|
.OrderBy(m => m.Date.Value)
|
||||||
|
.Take(8);
|
||||||
|
|
||||||
|
var recent =
|
||||||
|
allMatches
|
||||||
|
.Where(m => m.Completed.Value && m.Team1.Value != null && m.Team2.Value != null && Math.Abs(m.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < days_for_displays)
|
||||||
|
.OrderByDescending(m => m.Date.Value)
|
||||||
|
.Take(8);
|
||||||
|
|
||||||
ScheduleContainer comingUpNext;
|
ScheduleContainer comingUpNext;
|
||||||
|
|
||||||
@ -137,12 +154,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Width = 0.4f,
|
Width = 0.4f,
|
||||||
ChildrenEnumerable = ladder.Matches
|
ChildrenEnumerable = recent.Select(p => new ScheduleMatch(p))
|
||||||
.Where(p => p.Completed.Value && p.Team1.Value != null && p.Team2.Value != null
|
|
||||||
&& Math.Abs(p.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < 4)
|
|
||||||
.OrderByDescending(p => p.Date.Value)
|
|
||||||
.Take(8)
|
|
||||||
.Select(p => new ScheduleMatch(p))
|
|
||||||
},
|
},
|
||||||
new ScheduleContainer("upcoming matches")
|
new ScheduleContainer("upcoming matches")
|
||||||
{
|
{
|
||||||
@ -161,7 +173,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (match.NewValue != null)
|
if (currentMatch.Value != null)
|
||||||
{
|
{
|
||||||
comingUpNext.Child = new FillFlowContainer
|
comingUpNext.Child = new FillFlowContainer
|
||||||
{
|
{
|
||||||
@ -170,12 +182,12 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
Spacing = new Vector2(30),
|
Spacing = new Vector2(30),
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new ScheduleMatch(match.NewValue, false)
|
new ScheduleMatch(currentMatch.Value, false)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
},
|
},
|
||||||
new TournamentSpriteTextWithBackground(match.NewValue.Round.Value?.Name.Value ?? string.Empty)
|
new TournamentSpriteTextWithBackground(currentMatch.Value.Round.Value?.Name.Value ?? string.Empty)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
@ -185,7 +197,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Text = match.NewValue.Team1.Value?.FullName + " vs " + match.NewValue.Team2.Value?.FullName,
|
Text = currentMatch.Value.Team1.Value?.FullName + " vs " + currentMatch.Value.Team2.Value?.FullName,
|
||||||
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold)
|
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold)
|
||||||
},
|
},
|
||||||
new FillFlowContainer
|
new FillFlowContainer
|
||||||
@ -196,7 +208,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new ScheduleMatchDate(match.NewValue.Date.Value)
|
new ScheduleMatchDate(currentMatch.Value.Date.Value)
|
||||||
{
|
{
|
||||||
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular)
|
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular)
|
||||||
}
|
}
|
||||||
@ -282,6 +294,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
{
|
{
|
||||||
Direction = FillDirection.Vertical,
|
Direction = FillDirection.Vertical,
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Spacing = new Vector2(0, -6),
|
||||||
Margin = new MarginPadding(10)
|
Margin = new MarginPadding(10)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user