mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 17:32:54 +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.
|
||||
|
||||
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
|
||||
|
||||
|
@ -9,9 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<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="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
|
||||
|
@ -9,9 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<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="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||
|
@ -9,9 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<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="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
||||
|
@ -9,9 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<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="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||
|
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.823.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.914.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- 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="Mono.Posix.NETStandard" Version="1.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 Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
|
@ -7,9 +7,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.4" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.8" />
|
||||
<PackageReference Include="nunit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</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("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
||||
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]
|
||||
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
addPlacementSteps(times, positions);
|
||||
addPathCheckStep(times, positions);
|
||||
|
||||
AddAssert("slider velocity changed", () => !lastObject.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("slider velocity changed", () => !lastObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -108,11 +108,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
double[] times = { 100, 300 };
|
||||
float[] positions = { 200, 300 };
|
||||
addBlueprintStep(times, positions);
|
||||
AddAssert("default slider velocity", () => hitObject.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("default slider velocity", () => hitObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||
|
||||
addDragStartStep(times[1], positions[1]);
|
||||
AddMouseMoveStep(times[1], 400);
|
||||
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||
}
|
||||
|
||||
[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">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<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="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
|
||||
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
||||
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1
|
||||
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
|
||||
}.Yield();
|
||||
|
||||
case IHasDuration endTime:
|
||||
|
@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||
{
|
||||
// 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 requiredVelocity = path.ComputeRequiredVelocity();
|
||||
|
||||
|
@ -28,17 +28,17 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
public int RepeatCount { get; set; }
|
||||
|
||||
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
|
||||
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
|
||||
{
|
||||
Precision = 0.01,
|
||||
MinValue = 0.1,
|
||||
MaxValue = 10
|
||||
};
|
||||
|
||||
public double SliderVelocity
|
||||
public double SliderVelocityMultiplier
|
||||
{
|
||||
get => SliderVelocityBindable.Value;
|
||||
set => SliderVelocityBindable.Value = value;
|
||||
get => SliderVelocityMultiplierBindable.Value;
|
||||
set => SliderVelocityMultiplierBindable.Value = value;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
@ -48,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
private double tickDistanceFactor;
|
||||
|
||||
[JsonIgnore]
|
||||
public double Velocity => velocityFactor * SliderVelocity;
|
||||
public double Velocity => velocityFactor * SliderVelocityMultiplier;
|
||||
|
||||
[JsonIgnore]
|
||||
public double TickDistance => tickDistanceFactor * SliderVelocity;
|
||||
public double TickDistance => tickDistanceFactor * SliderVelocityMultiplier;
|
||||
|
||||
/// <summary>
|
||||
/// 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">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<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="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
@ -10,10 +10,11 @@ using System.Linq;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Game.Audio;
|
||||
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.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Utils;
|
||||
|
||||
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);
|
||||
|
||||
double beatLength;
|
||||
if (hitObject.LegacyBpmMultiplier.HasValue)
|
||||
beatLength = timingPoint.BeatLength * hitObject.LegacyBpmMultiplier.Value;
|
||||
else if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
||||
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
|
||||
|
||||
if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
||||
beatLength = LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(hasSliderVelocity, timingPoint, ManiaRuleset.SHORT_NAME);
|
||||
else
|
||||
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.
|
||||
// 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.Scoring;
|
||||
|
||||
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.
|
||||
// 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.Scoring;
|
||||
|
||||
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.
|
||||
// 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.Scoring;
|
||||
|
||||
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.
|
||||
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Scoring
|
||||
{
|
||||
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)
|
||||
{
|
||||
switch (result)
|
||||
@ -22,5 +35,12 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
|
||||
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;
|
||||
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))
|
||||
}),
|
||||
RepeatCount = 1,
|
||||
SliderVelocity = 10
|
||||
SliderVelocityMultiplier = 10
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[TestCase("basic")]
|
||||
[TestCase("colinear-perfect-curve")]
|
||||
[TestCase("slider-ticks")]
|
||||
[TestCase("slider-ticks-edge-case")]
|
||||
[TestCase("repeat-slider")]
|
||||
[TestCase("uneven-repeat-slider")]
|
||||
[TestCase("old-stacking")]
|
||||
|
@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
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
|
||||
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.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
private float? alphaAtMiss;
|
||||
|
||||
[Test]
|
||||
public void TestHitCircleClassicMod()
|
||||
public void TestHitCircleClassicModMiss()
|
||||
{
|
||||
AddStep("Create hit circle", () =>
|
||||
{
|
||||
@ -61,8 +62,27 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No early fade is expected to be applied if the hit circle has been hit.
|
||||
/// </summary>
|
||||
[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", () =>
|
||||
{
|
||||
@ -74,6 +94,16 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddAssert("Opaque when missed", () => alphaAtMiss == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitCircleNoModHit()
|
||||
{
|
||||
AddStep("Create hit circle", () =>
|
||||
{
|
||||
SelectedMods.Value = Array.Empty<Mod>();
|
||||
createCircle(true);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderClassicMod()
|
||||
{
|
||||
@ -100,27 +130,33 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddAssert("Head circle opaque when missed", () => alphaAtMiss == 1);
|
||||
}
|
||||
|
||||
private void createCircle()
|
||||
private TestDrawableHitCircle createCircle(bool shouldHit = false)
|
||||
{
|
||||
alphaAtMiss = null;
|
||||
|
||||
DrawableHitCircle drawableHitCircle = new DrawableHitCircle(new HitCircle
|
||||
TestDrawableHitCircle drawableHitCircle = new TestDrawableHitCircle(new HitCircle
|
||||
{
|
||||
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>())
|
||||
mod.ApplyToDrawableHitObject(drawableHitCircle);
|
||||
|
||||
drawableHitCircle.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
drawableHitCircle.OnNewResult += (_, _) =>
|
||||
drawableHitCircle.OnNewResult += (_, result) =>
|
||||
{
|
||||
alphaAtMiss = drawableHitCircle.Alpha;
|
||||
if (!result.IsHit)
|
||||
alphaAtMiss = drawableHitCircle.Alpha;
|
||||
};
|
||||
|
||||
Child = drawableHitCircle;
|
||||
|
||||
return drawableHitCircle;
|
||||
}
|
||||
|
||||
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.OnLoadComplete += _ =>
|
||||
@ -145,12 +183,36 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||
mod.ApplyToDrawableHitObject(drawableSlider.HeadCircle);
|
||||
|
||||
drawableSlider.HeadCircle.OnNewResult += (_, _) =>
|
||||
drawableSlider.HeadCircle.OnNewResult += (_, result) =>
|
||||
{
|
||||
alphaAtMiss = drawableSlider.HeadCircle.Alpha;
|
||||
if (!result.IsHit)
|
||||
alphaAtMiss = drawableSlider.HeadCircle.Alpha;
|
||||
};
|
||||
};
|
||||
|
||||
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.TypeExtensions;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
@ -32,7 +36,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public partial class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene
|
||||
public partial class TestSceneLegacyHitPolicy : RateAdjustedBeatmapTestScene
|
||||
{
|
||||
private readonly OsuHitWindows referenceHitWindows;
|
||||
|
||||
@ -43,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
/// </summary>
|
||||
private readonly string? exportLocation = null;
|
||||
|
||||
public TestSceneObjectOrderedHitPolicy()
|
||||
public TestSceneLegacyHitPolicy()
|
||||
{
|
||||
referenceHitWindows = new OsuHitWindows();
|
||||
referenceHitWindows.SetDifficulty(0);
|
||||
@ -83,6 +87,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
addJudgementAssert(hitObjects[1], 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -119,6 +124,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
addJudgementAssert(hitObjects[1], 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -155,6 +161,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
addJudgementAssert(hitObjects[1], 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -191,7 +198,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
addJudgementAssert(hitObjects[0], HitResult.Meh);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
||||
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>
|
||||
@ -229,13 +238,15 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
addJudgementAssert(hitObjects[1], HitResult.Ok);
|
||||
addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
|
||||
addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time
|
||||
addClickActionAssert(0, ClickAction.Hit);
|
||||
addClickActionAssert(1, ClickAction.Hit);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
[Test]
|
||||
public void TestMissSliderHeadAndHitAllSliderTicks()
|
||||
public void TestHitCircleBeforeSliderHead()
|
||||
{
|
||||
const double time_slider = 1500;
|
||||
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 } }
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
||||
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
||||
addClickActionAssert(0, ClickAction.Hit);
|
||||
addClickActionAssert(1, ClickAction.Hit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -314,6 +327,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
||||
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
||||
addClickActionAssert(0, ClickAction.Hit);
|
||||
addClickActionAssert(1, ClickAction.Hit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -353,6 +368,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
||||
addClickActionAssert(0, ClickAction.Hit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -391,6 +407,291 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
addJudgementAssert(hitObjects[0], 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)
|
||||
@ -408,17 +709,36 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
private void addJudgementOffsetAssert(OsuHitObject hitObject, double 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 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!;
|
||||
Score score = null!;
|
||||
|
||||
AddStep("set up mods", () =>
|
||||
{
|
||||
mods = new List<Mod> { new OsuModClassic() };
|
||||
|
||||
if (extraMods != null)
|
||||
mods.AddRange(extraMods);
|
||||
});
|
||||
|
||||
AddStep("create beatmap", () =>
|
||||
{
|
||||
var cpi = new ControlPointInfo();
|
||||
@ -461,7 +781,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
ScoreInfo =
|
||||
{
|
||||
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", () =>
|
||||
{
|
||||
SelectedMods.Value = new[] { new OsuModClassic() };
|
||||
SelectedMods.Value = mods.ToArray();
|
||||
|
||||
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("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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Audio;
|
||||
@ -19,7 +20,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
@ -35,16 +35,24 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
private int depthIndex;
|
||||
|
||||
private readonly BindableBool snakingIn = new BindableBool();
|
||||
private readonly BindableBool snakingOut = new BindableBool();
|
||||
private readonly BindableBool snakingIn = new BindableBool(true);
|
||||
private readonly BindableBool snakingOut = new BindableBool(true);
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
private float progressToHit;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
AddToggleStep("toggle snaking", v =>
|
||||
base.LoadComplete();
|
||||
|
||||
AddToggleStep("disable snaking", v =>
|
||||
{
|
||||
snakingIn.Value = v;
|
||||
snakingOut.Value = v;
|
||||
snakingIn.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);
|
||||
}
|
||||
|
||||
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]
|
||||
public void TestVariousSliders()
|
||||
{
|
||||
@ -206,7 +226,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
StackHeight = 10
|
||||
};
|
||||
|
||||
return createDrawable(slider, 2, 2);
|
||||
return createDrawable(slider, 2);
|
||||
}
|
||||
|
||||
private Drawable testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats);
|
||||
@ -229,6 +249,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
SliderVelocityMultiplier = speedMultiplier,
|
||||
StartTime = Time.Current + time_offset,
|
||||
Position = new Vector2(0, -(distance / 2)),
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
@ -240,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
StackHeight = stackHeight
|
||||
};
|
||||
|
||||
return createDrawable(slider, circleSize, speedMultiplier);
|
||||
return createDrawable(slider, circleSize);
|
||||
}
|
||||
|
||||
private Drawable testPerfect(int repeats = 0)
|
||||
@ -258,7 +279,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
RepeatCount = repeats,
|
||||
};
|
||||
|
||||
return createDrawable(slider, 2, 3);
|
||||
return createDrawable(slider, 2);
|
||||
}
|
||||
|
||||
private Drawable testLinear(int repeats = 0) => createLinear(repeats);
|
||||
@ -281,7 +302,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
RepeatCount = repeats,
|
||||
};
|
||||
|
||||
return createDrawable(slider, 2, 3);
|
||||
return createDrawable(slider, 2);
|
||||
}
|
||||
|
||||
private Drawable testBezier(int repeats = 0) => createBezier(repeats);
|
||||
@ -303,7 +324,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
RepeatCount = repeats,
|
||||
};
|
||||
|
||||
return createDrawable(slider, 2, 3);
|
||||
return createDrawable(slider, 2);
|
||||
}
|
||||
|
||||
private Drawable testLinearOverlapping(int repeats = 0) => createOverlapping(repeats);
|
||||
@ -326,7 +347,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
RepeatCount = repeats,
|
||||
};
|
||||
|
||||
return createDrawable(slider, 2, 3);
|
||||
return createDrawable(slider, 2);
|
||||
}
|
||||
|
||||
private Drawable testCatmull(int repeats = 0) => createCatmull(repeats);
|
||||
@ -352,15 +373,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
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();
|
||||
cpi.Add(0, new DifficultyControlPoint { SliderVelocity = speedMultiplier });
|
||||
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty
|
||||
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty
|
||||
{
|
||||
CircleSize = circleSize,
|
||||
SliderTickRate = 3
|
||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
StartTime = time_slider_start,
|
||||
Position = new Vector2(0, 0),
|
||||
SliderVelocity = velocity,
|
||||
SliderVelocityMultiplier = velocity,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
|
@ -349,7 +349,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
StartTime = time_slider_start,
|
||||
Position = new Vector2(0, 0),
|
||||
SliderVelocity = 0.1f,
|
||||
SliderVelocityMultiplier = 0.1f,
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -33,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
public partial class TestSceneSliderSnaking : TestSceneOsuPlayer
|
||||
{
|
||||
[Resolved]
|
||||
private AudioManager audioManager { get; set; }
|
||||
private AudioManager audioManager { get; set; } = null!;
|
||||
|
||||
protected override bool Autoplay => autoplay;
|
||||
private bool autoplay;
|
||||
@ -41,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
private readonly BindableBool snakingIn = 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 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);
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -57,15 +55,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
|
||||
}
|
||||
|
||||
private Slider slider;
|
||||
private DrawableSlider drawableSlider;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
slider = null;
|
||||
drawableSlider = null;
|
||||
});
|
||||
private Slider slider = null!;
|
||||
private DrawableSlider? drawableSlider;
|
||||
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
@ -135,9 +126,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRepeatArrowDoesNotMoveWhenHit()
|
||||
public void TestRepeatArrowDoesNotMove([Values] bool useAutoplay)
|
||||
{
|
||||
AddStep("enable autoplay", () => autoplay = true);
|
||||
AddStep($"set autoplay to {useAutoplay}", () => autoplay = useAutoplay);
|
||||
setSnaking(true);
|
||||
CreateTest();
|
||||
// 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);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRepeatArrowMovesWhenNotHit()
|
||||
{
|
||||
AddStep("disable autoplay", () => autoplay = false);
|
||||
setSnaking(true);
|
||||
CreateTest();
|
||||
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased);
|
||||
}
|
||||
|
||||
private void retrieveSlider(int index)
|
||||
{
|
||||
AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]);
|
||||
addSeekStep(() => slider.StartTime);
|
||||
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);
|
||||
@ -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<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 getSliderEnd() => getSliderCurve().Last();
|
||||
|
||||
|
@ -336,6 +336,52 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
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)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
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 List<JudgementResult> judgementResults;
|
||||
|
||||
@ -399,7 +451,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public TestSlider()
|
||||
{
|
||||
SliderVelocity = 0.1f;
|
||||
SliderVelocityMultiplier = 0.1f;
|
||||
|
||||
DefaultsApplied += _ =>
|
||||
{
|
||||
|
@ -1,10 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<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="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<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.
|
||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
|
||||
GenerateTicks = generateTicksData?.GenerateTicks ?? true,
|
||||
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1,
|
||||
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1,
|
||||
}.Yield();
|
||||
|
||||
case IHasDuration endTimeData:
|
||||
|
@ -85,9 +85,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
BeginPlacement();
|
||||
|
||||
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);
|
||||
|
||||
// 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.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
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.")]
|
||||
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.")]
|
||||
public Bindable<bool> ClassicNoteLock { get; } = new BindableBool(true);
|
||||
|
||||
@ -57,7 +55,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
var osuRuleset = (DrawableOsuRuleset)drawableRuleset;
|
||||
|
||||
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;
|
||||
}
|
||||
@ -67,9 +68,12 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
switch (obj)
|
||||
{
|
||||
case DrawableSliderHead head:
|
||||
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
|
||||
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
||||
applyEarlyFading(head);
|
||||
|
||||
if (ClassicNoteLock.Value)
|
||||
blockInputToObjectsUnderSliderHead(head);
|
||||
|
||||
break;
|
||||
|
||||
case DrawableSliderTail tail:
|
||||
@ -79,19 +83,39 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
case DrawableHitCircle circle:
|
||||
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
||||
applyEarlyFading(circle);
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
double lateMissFadeTime = o.HitObject.HitWindows.WindowFor(HitResult.Meh) - okWindow;
|
||||
o.Delay(okWindow).FadeOut(lateMissFadeTime);
|
||||
if (state != ArmedState.Hit)
|
||||
{
|
||||
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.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
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))]
|
||||
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
|
||||
@ -20,12 +26,13 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
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
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
ExtendedMinValue = -10,
|
||||
ExtendedMaxValue = 11,
|
||||
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
|
||||
};
|
||||
@ -53,5 +60,34 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.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)
|
||||
{
|
||||
// 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.
|
||||
this.TransformTo(nameof(FlashlightDim), e.NewValue ? 0.8f : 0.0f, 50);
|
||||
// If a slider is in a tracking state, a further dim should be applied to the (remaining) visible portion of the playfield.
|
||||
FlashlightDim = e.NewValue ? 0.8f : 0.0f;
|
||||
}
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
|
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
ComboOffset = original.ComboOffset;
|
||||
LegacyLastTickOffset = original.LegacyLastTickOffset;
|
||||
TickDistanceMultiplier = original.TickDistanceMultiplier;
|
||||
SliderVelocity = original.SliderVelocity;
|
||||
SliderVelocityMultiplier = original.SliderVelocityMultiplier;
|
||||
}
|
||||
|
||||
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.Skinning;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
@ -154,12 +155,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if (result == HitResult.None || clickAction != ClickAction.Hit)
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyResult(r =>
|
||||
{
|
||||
@ -259,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
case OsuAction.RightButton:
|
||||
if (IsHovered && (Hit?.Invoke() ?? false))
|
||||
{
|
||||
HitAction = e.Action;
|
||||
HitAction ??= e.Action;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,8 @@ using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -30,10 +32,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
protected override float SamplePlaybackPosition => CalculateDrawableRelativePosition(this);
|
||||
|
||||
/// <summary>
|
||||
/// Whether this <see cref="DrawableOsuHitObject"/> can be hit, given a time value.
|
||||
/// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false.
|
||||
/// What action this <see cref="DrawableOsuHitObject"/> should take in response to a
|
||||
/// 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>
|
||||
public Func<DrawableHitObject, double, bool> CheckHittable;
|
||||
public Func<DrawableHitObject, double, HitResult, ClickAction> CheckHittable;
|
||||
|
||||
protected DrawableOsuHitObject(OsuHitObject 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);
|
||||
|
||||
Ball.UpdateProgress(completionProgress);
|
||||
SliderBody?.UpdateProgress(completionProgress);
|
||||
SliderBody?.UpdateProgress(HeadCircle.IsHit ? completionProgress : 0);
|
||||
|
||||
foreach (DrawableHitObject hitObject in NestedHitObjects)
|
||||
{
|
||||
@ -317,7 +317,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
switch (state)
|
||||
{
|
||||
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.
|
||||
break;
|
||||
}
|
||||
|
@ -3,11 +3,10 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
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;
|
||||
|
||||
/// <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>();
|
||||
|
||||
protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle;
|
||||
@ -60,24 +53,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
pathVersion.BindTo(DrawableSlider.PathVersion);
|
||||
|
||||
CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? true;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
CheckHittable = (d, t, r) => DrawableSlider.CheckHittable?.Invoke(d, t, r) ?? ClickAction.Hit;
|
||||
}
|
||||
|
||||
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 int wholeSpins;
|
||||
private int completedFullSpins;
|
||||
|
||||
private void updateBonusScore()
|
||||
{
|
||||
@ -295,14 +295,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
int spins = (int)(Result.RateAdjustedRotation / 360);
|
||||
|
||||
if (spins < wholeSpins)
|
||||
if (spins < completedFullSpins)
|
||||
{
|
||||
// rewinding, silently handle
|
||||
wholeSpins = spins;
|
||||
completedFullSpins = spins;
|
||||
return;
|
||||
}
|
||||
|
||||
while (wholeSpins != spins)
|
||||
while (completedFullSpins != spins)
|
||||
{
|
||||
var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
|
||||
|
||||
@ -312,10 +312,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
tick.TriggerResult(true);
|
||||
|
||||
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.ControlPoints;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
@ -113,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
public double SpanDuration => Duration / this.SpanCount();
|
||||
|
||||
/// <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>
|
||||
public double Velocity { get; private set; }
|
||||
|
||||
@ -134,17 +135,16 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// </summary>
|
||||
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,
|
||||
MaxValue = 10
|
||||
};
|
||||
|
||||
public double SliderVelocity
|
||||
public double SliderVelocityMultiplier
|
||||
{
|
||||
get => SliderVelocityBindable.Value;
|
||||
set => SliderVelocityBindable.Value = value;
|
||||
get => SliderVelocityMultiplierBindable.Value;
|
||||
set => SliderVelocityMultiplierBindable.Value = value;
|
||||
}
|
||||
|
||||
public bool GenerateTicks { get; set; } = true;
|
||||
@ -167,9 +167,11 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,16 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// </summary>
|
||||
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>
|
||||
/// Number of spins available to give bonus, beyond <see cref="SpinsRequired"/>.
|
||||
/// </summary>
|
||||
@ -42,25 +52,20 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
// spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being.
|
||||
const double stable_matching_fudge = 0.6;
|
||||
|
||||
// close to 477rpm
|
||||
const double maximum_rotations_per_second = 8;
|
||||
const double maximum_rotations_per_second = 477f / 60f;
|
||||
|
||||
double secondsDuration = Duration / 1000;
|
||||
|
||||
double minimumRotationsPerSecond = stable_matching_fudge * IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
|
||||
double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 1.5, 2.5, 3.75);
|
||||
|
||||
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)
|
||||
{
|
||||
base.CreateNestedHitObjects(cancellationToken);
|
||||
|
||||
int totalSpins = MaximumBonusSpins + SpinsRequired;
|
||||
int totalSpins = MaximumBonusSpins + SpinsRequired + bonus_spins_gap;
|
||||
|
||||
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;
|
||||
|
||||
AddNested(i < SpinsRequired
|
||||
AddNested(i < SpinsRequiredForBonus
|
||||
? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration }
|
||||
: 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;
|
||||
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) =>
|
||||
|
@ -1,9 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI
|
||||
@ -13,9 +12,9 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
|
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)
|
||||
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)),
|
||||
TexturePosition = textureRect.BottomLeft,
|
||||
@ -295,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
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)),
|
||||
TexturePosition = textureRect.BottomRight,
|
||||
@ -304,7 +304,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
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),
|
||||
TexturePosition = textureRect.TopRight,
|
||||
@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
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),
|
||||
TexturePosition = textureRect.TopLeft,
|
||||
@ -362,22 +362,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
[VertexMember(1, VertexAttribPointerType.Float)]
|
||||
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)
|
||||
{
|
||||
return Position.Equals(other.Position)
|
||||
&& TexturePosition.Equals(other.TexturePosition)
|
||||
&& Colour.Equals(other.Colour)
|
||||
&& Time.Equals(other.Time)
|
||||
&& maskingIndex == other.maskingIndex;
|
||||
&& Time.Equals(other.Time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI
|
||||
@ -19,8 +20,9 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> 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>
|
||||
bool IsHittable(DrawableHitObject hitObject, double time);
|
||||
ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result);
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable;
|
||||
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.CheckHittable;
|
||||
|
||||
Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}");
|
||||
drawable.OnLoadComplete += onDrawableHitObjectLoaded;
|
||||
|
@ -1,13 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI
|
||||
@ -22,11 +21,14 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
/// </summary>
|
||||
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))
|
||||
{
|
||||
@ -36,22 +38,25 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
// If there is no previous hitobject, allow the hit.
|
||||
if (blockingObject == null)
|
||||
return true;
|
||||
return ClickAction.Hit;
|
||||
|
||||
// A hit is allowed if:
|
||||
// 1. The last blocking hitobject has been judged.
|
||||
// 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).
|
||||
return blockingObject.Judged || time >= blockingObject.HitObject.StartTime;
|
||||
return (blockingObject.Judged || time >= blockingObject.HitObject.StartTime) ? ClickAction.Hit : ClickAction.Shake;
|
||||
}
|
||||
|
||||
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).
|
||||
if (!hitObjectCanBlockFutureHits(hitObject))
|
||||
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!");
|
||||
|
||||
// Miss all hitobjects prior to the hit one.
|
||||
@ -74,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
|
||||
{
|
||||
foreach (var obj in HitObjectContainer.AliveObjects)
|
||||
foreach (var obj in HitObjectContainer!.AliveObjects)
|
||||
{
|
||||
if (obj.HitObject.StartTime >= targetTime)
|
||||
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">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<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="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
@ -14,6 +14,7 @@ using JetBrains.Annotations;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
{
|
||||
@ -64,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
{
|
||||
if (hitObject is not IHasSliderVelocity hasSliderVelocity) continue;
|
||||
|
||||
double nextScrollSpeed = hasSliderVelocity.SliderVelocity;
|
||||
double nextScrollSpeed = hasSliderVelocity.SliderVelocityMultiplier;
|
||||
EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime);
|
||||
|
||||
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);
|
||||
|
||||
double beatLength;
|
||||
if (obj.LegacyBpmMultiplier.HasValue)
|
||||
beatLength = timingPoint.BeatLength * obj.LegacyBpmMultiplier.Value;
|
||||
else if (obj is IHasSliderVelocity hasSliderVelocity)
|
||||
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
|
||||
|
||||
if (obj is IHasSliderVelocity hasSliderVelocity)
|
||||
beatLength = LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(hasSliderVelocity, timingPoint, TaikoRuleset.SHORT_NAME);
|
||||
else
|
||||
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]
|
||||
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(3000).SliderVelocity, Is.EqualTo(1));
|
||||
|
||||
#pragma warning disable 618
|
||||
Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(2000)).GenerateTicks, Is.False);
|
||||
Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(3000)).GenerateTicks, Is.True);
|
||||
#pragma warning restore 618
|
||||
Assert.That(controlPoints.DifficultyPointAt(2000).GenerateTicks, Is.False);
|
||||
Assert.That(controlPoints.DifficultyPointAt(3000).GenerateTicks, Is.True);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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." });
|
||||
|
||||
Assert.AreEqual(result.Content, result.DisplayContent);
|
||||
Assert.AreEqual(1, result.Links.Count);
|
||||
Assert.AreEqual("gopher://really-old-protocol", result.Links[0].Url);
|
||||
Assert.AreEqual(0, result.Links.Count);
|
||||
}
|
||||
|
||||
[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]
|
||||
|
@ -78,7 +78,7 @@ namespace osu.Game.Tests.Editing
|
||||
{
|
||||
assertSnapDistance(100, new Slider
|
||||
{
|
||||
SliderVelocity = multiplier
|
||||
SliderVelocityMultiplier = multiplier
|
||||
}, false);
|
||||
}
|
||||
|
||||
@ -88,7 +88,7 @@ namespace osu.Game.Tests.Editing
|
||||
{
|
||||
assertSnapDistance(100 * multiplier, new Slider
|
||||
{
|
||||
SliderVelocity = multiplier
|
||||
SliderVelocityMultiplier = multiplier
|
||||
}, true);
|
||||
}
|
||||
|
||||
@ -112,7 +112,7 @@ namespace osu.Game.Tests.Editing
|
||||
|
||||
var referenceObject = new Slider
|
||||
{
|
||||
SliderVelocity = slider_velocity
|
||||
SliderVelocityMultiplier = slider_velocity
|
||||
};
|
||||
|
||||
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
|
||||
|
@ -13,7 +13,7 @@ layout(location = 4) out mediump vec2 v_BlendRange;
|
||||
void main(void)
|
||||
{
|
||||
// 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_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));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNano()
|
||||
{
|
||||
createTestCase(beatmapSetInfo => new BeatmapCardNano(beatmapSetInfo));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormal()
|
||||
{
|
||||
|
@ -47,7 +47,35 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
||||
pill.AutoSizeAxes = Axes.Y;
|
||||
pill.Width = 90;
|
||||
}));
|
||||
|
||||
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))
|
||||
}
|
||||
},
|
||||
SliderVelocity = 2
|
||||
SliderVelocityMultiplier = 2
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("unify slider velocity", () =>
|
||||
{
|
||||
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));
|
||||
@ -194,7 +194,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () =>
|
||||
{
|
||||
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)
|
||||
{
|
||||
// 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;
|
||||
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.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@ -16,6 +17,7 @@ using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.GameplayTest;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -203,6 +205,33 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
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 Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;
|
||||
|
@ -64,6 +64,9 @@ namespace osu.Game.Tests.Visual.Online
|
||||
addMessageWithChecks("test!");
|
||||
addMessageWithChecks("dev.ppy.sh!");
|
||||
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("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);
|
||||
@ -84,9 +87,11 @@ namespace osu.Game.Tests.Visual.Online
|
||||
addMessageWithChecks("feels important", 0, true, true);
|
||||
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 gameosump://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 {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 #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");
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
[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),
|
||||
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",
|
||||
},
|
||||
new Badge
|
||||
{
|
||||
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
|
||||
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",
|
||||
|
@ -5,20 +5,29 @@ using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Profile.Header.Components;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneUserProfilePreviousUsernames : OsuTestScene
|
||||
public partial class TestSceneUserProfilePreviousUsernamesDisplay : OsuTestScene
|
||||
{
|
||||
private PreviousUsernames container = null!;
|
||||
private PreviousUsernamesDisplay container = null!;
|
||||
private OverlayColourProvider colourProvider = null!;
|
||||
|
||||
[SetUp]
|
||||
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,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
@ -39,6 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
private BeatmapInfo currentSelection => carousel.SelectedBeatmapInfo;
|
||||
|
||||
private const int set_count = 5;
|
||||
private const int diff_count = 3;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetStore rulesets)
|
||||
@ -111,7 +112,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Test]
|
||||
public void TestScrollPositionMaintainedOnAdd()
|
||||
{
|
||||
loadBeatmaps(count: 1, randomDifficulties: false);
|
||||
loadBeatmaps(setCount: 1);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
@ -124,7 +125,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Test]
|
||||
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));
|
||||
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]
|
||||
public void TestScrollPositionMaintainedOnDelete()
|
||||
{
|
||||
loadBeatmaps(count: 50, randomDifficulties: false);
|
||||
loadBeatmaps(setCount: 50);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
@ -150,7 +151,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Test]
|
||||
public void TestManyPanels()
|
||||
{
|
||||
loadBeatmaps(count: 5000, randomDifficulties: true);
|
||||
loadBeatmaps(setCount: 5000, randomDifficulties: true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -501,6 +502,34 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
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]
|
||||
public void TestSelectionEnteringFromEmptyRuleset()
|
||||
{
|
||||
@ -662,7 +691,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
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.
|
||||
var beatmap = set.Beatmaps.First();
|
||||
@ -709,7 +738,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
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.
|
||||
var beatmap = set.Beatmaps.First();
|
||||
@ -758,32 +787,54 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
|
||||
[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>();
|
||||
|
||||
AddStep("Populuate beatmap sets", () =>
|
||||
{
|
||||
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[2].StarRating = 6 + i;
|
||||
set.Beatmaps[1].StarRating = 6 + i;
|
||||
sets.Add(set);
|
||||
}
|
||||
});
|
||||
|
||||
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));
|
||||
AddAssert("Check first set at end", () => carousel.BeatmapSets.First().Equals(sets.Last()));
|
||||
AddAssert("Check last set at start", () => carousel.BeatmapSets.Last().Equals(sets.First()));
|
||||
checkVisibleItemCount(false, local_set_count);
|
||||
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));
|
||||
AddAssert("Check first set at start", () => carousel.BeatmapSets.First().Equals(sets.First()));
|
||||
AddAssert("Check last set at end", () => carousel.BeatmapSets.Last().Equals(sets.Last()));
|
||||
checkVisibleItemCount(false, local_set_count);
|
||||
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]
|
||||
@ -838,7 +889,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
AddStep("create hidden set", () =>
|
||||
{
|
||||
hidingSet = TestResources.CreateTestBeatmapSetInfo(3);
|
||||
hidingSet = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||
hidingSet.Beatmaps[1].Hidden = true;
|
||||
|
||||
hiddenList.Clear();
|
||||
@ -885,7 +936,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
AddStep("add mixed ruleset beatmapset", () =>
|
||||
{
|
||||
testMixed = TestResources.CreateTestBeatmapSetInfo(3);
|
||||
testMixed = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||
|
||||
for (int i = 0; i <= 2; i++)
|
||||
{
|
||||
@ -907,7 +958,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
BeatmapSetInfo testSingle = null;
|
||||
AddStep("add single ruleset beatmapset", () =>
|
||||
{
|
||||
testSingle = TestResources.CreateTestBeatmapSetInfo(3);
|
||||
testSingle = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||
testSingle.Beatmaps.ForEach(b =>
|
||||
{
|
||||
b.Ruleset = rulesets.AvailableRulesets.ElementAt(1);
|
||||
@ -930,7 +981,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
manySets.Clear();
|
||||
|
||||
for (int i = 1; i <= 50; i++)
|
||||
manySets.Add(TestResources.CreateTestBeatmapSetInfo(3));
|
||||
manySets.Add(TestResources.CreateTestBeatmapSetInfo(diff_count));
|
||||
});
|
||||
|
||||
loadBeatmaps(manySets);
|
||||
@ -955,6 +1006,43 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
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]
|
||||
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,
|
||||
bool randomDifficulties = false)
|
||||
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null,
|
||||
int? setCount = null, int? diffCount = null, bool randomDifficulties = false)
|
||||
{
|
||||
bool changed = false;
|
||||
|
||||
if (beatmapSets == null)
|
||||
{
|
||||
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(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.ControlPoints;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
@ -188,7 +189,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Select.FooterV2;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -37,10 +39,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
footer = new FooterV2
|
||||
new PopoverContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = footer = new FooterV2(),
|
||||
},
|
||||
overlay = new DummyOverlay()
|
||||
};
|
||||
@ -56,6 +58,24 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
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]
|
||||
public void TestState()
|
||||
{
|
||||
|
@ -15,6 +15,7 @@ using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Tests.Online;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
@ -192,6 +193,57 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
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()
|
||||
{
|
||||
return carousel = new BeatmapCarousel
|
||||
@ -199,7 +251,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
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("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0));
|
||||
AddStep("reset mods", () => SelectedMods.SetDefault());
|
||||
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo));
|
||||
AddStep("set up presets", () =>
|
||||
{
|
||||
Realm.Write(r =>
|
||||
@ -92,6 +93,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
State = { Value = Visibility.Visible },
|
||||
Beatmap = Beatmap.Value,
|
||||
SelectedMods = { BindTarget = SelectedMods }
|
||||
});
|
||||
waitForColumnLoad();
|
||||
@ -113,7 +115,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("mod multiplier correct", () =>
|
||||
{
|
||||
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);
|
||||
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
|
||||
@ -128,7 +130,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("mod multiplier correct", () =>
|
||||
{
|
||||
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);
|
||||
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.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,
|
||||
// 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("reset half time speed to default", () => modSelectOverlay.ChildrenOfType<ModSettingsArea>().Single()
|
||||
.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",
|
||||
|
@ -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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
@ -12,17 +11,17 @@ using osu.Game.Overlays.Mods;
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneDifficultyMultiplierDisplay : OsuTestScene
|
||||
public partial class TestSceneScoreMultiplierDisplay : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
||||
|
||||
[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,
|
||||
Origin = Anchor.Centre
|
||||
@ -34,7 +33,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
|
||||
AddSliderStep("set multiplier", 0, 2, 1d, multiplier =>
|
||||
{
|
||||
if (multiplierDisplay != null)
|
||||
if (multiplierDisplay.IsNotNull())
|
||||
multiplierDisplay.Current.Value = multiplier;
|
||||
});
|
||||
}
|
@ -2,10 +2,10 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<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="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" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
|
@ -58,9 +58,14 @@ namespace osu.Game.Tournament.Tests.Components
|
||||
|
||||
songBar.Beatmap = new TournamentBeatmap(beatmap);
|
||||
});
|
||||
|
||||
AddStep("set mods to HR", () => songBar.Mods = LegacyMods.HardRock);
|
||||
AddStep("set mods to DT", () => songBar.Mods = LegacyMods.DoubleTime);
|
||||
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 override void SetUpSteps()
|
||||
{
|
||||
AddStep("clear matches", () => Ladder.Matches.Clear());
|
||||
|
||||
base.SetUpSteps();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@ -34,6 +41,36 @@ namespace osu.Game.Tournament.Tests.Screens
|
||||
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)
|
||||
// Humanizer cannot handle negative timespans.
|
||||
=> AddStep($"start time is {relativeTime}", () =>
|
||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Tournament.Tests
|
||||
{
|
||||
public TournamentScalingContainer()
|
||||
{
|
||||
TargetDrawSize = new Vector2(1920, 1080);
|
||||
TargetDrawSize = new Vector2(1024, 768);
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,9 @@
|
||||
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
|
||||
</PropertyGroup>
|
||||
<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="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
@ -92,8 +92,16 @@ namespace osu.Game.Tournament.IPC
|
||||
else
|
||||
{
|
||||
beatmapLookupRequest = new GetBeatmapRequest(new APIBeatmap { OnlineID = beatmapId });
|
||||
beatmapLookupRequest.Success += b => Beatmap.Value = new TournamentBeatmap(b);
|
||||
beatmapLookupRequest.Failure += _ => Beatmap.Value = null;
|
||||
beatmapLookupRequest.Success += b =>
|
||||
{
|
||||
if (lastBeatmapId == beatmapId)
|
||||
Beatmap.Value = new TournamentBeatmap(b);
|
||||
};
|
||||
beatmapLookupRequest.Failure += _ =>
|
||||
{
|
||||
if (lastBeatmapId == beatmapId)
|
||||
Beatmap.Value = null;
|
||||
};
|
||||
API.Queue(beatmapLookupRequest);
|
||||
}
|
||||
}
|
||||
|
@ -7,13 +7,16 @@ using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
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.Online.Multiplayer;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tournament
|
||||
{
|
||||
internal partial class SaveChangesOverlay : CompositeDrawable
|
||||
internal partial class SaveChangesOverlay : CompositeDrawable, IKeyBindingHandler<PlatformAction>
|
||||
{
|
||||
[Resolved]
|
||||
private TournamentGame tournamentGame { get; set; } = null!;
|
||||
@ -78,6 +81,21 @@ namespace osu.Game.Tournament
|
||||
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 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)
|
||||
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)
|
||||
addForBeatmap(beatmap.NewValue.OnlineID);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -19,6 +20,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
||||
{
|
||||
public partial class ScheduleScreen : TournamentScreen
|
||||
{
|
||||
private readonly BindableList<TournamentMatch> allMatches = new BindableList<TournamentMatch>();
|
||||
private readonly Bindable<TournamentMatch?> currentMatch = new Bindable<TournamentMatch?>();
|
||||
private Container mainContainer = null!;
|
||||
private LadderInfo ladder = null!;
|
||||
@ -101,19 +103,34 @@ namespace osu.Game.Tournament.Screens.Schedule
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
allMatches.BindTo(ladder.Matches);
|
||||
allMatches.BindCollectionChanged((_, _) => refresh());
|
||||
|
||||
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);
|
||||
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)
|
||||
.SelectMany(m => m.ConditionalMatches.Where(cp => m.Acronyms.TrueForAll(a => cp.Acronyms.Contains(a))));
|
||||
const int days_for_displays = 4;
|
||||
|
||||
upcoming = upcoming.Concat(conditionals);
|
||||
upcoming = upcoming.OrderBy(p => p.Date.Value).Take(8);
|
||||
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))));
|
||||
|
||||
IEnumerable<TournamentMatch> upcoming =
|
||||
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;
|
||||
|
||||
@ -137,12 +154,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.4f,
|
||||
ChildrenEnumerable = 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)
|
||||
.OrderByDescending(p => p.Date.Value)
|
||||
.Take(8)
|
||||
.Select(p => new ScheduleMatch(p))
|
||||
ChildrenEnumerable = recent.Select(p => new ScheduleMatch(p))
|
||||
},
|
||||
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
|
||||
{
|
||||
@ -170,12 +182,12 @@ namespace osu.Game.Tournament.Screens.Schedule
|
||||
Spacing = new Vector2(30),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ScheduleMatch(match.NewValue, false)
|
||||
new ScheduleMatch(currentMatch.Value, false)
|
||||
{
|
||||
Anchor = 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,
|
||||
Origin = Anchor.CentreLeft,
|
||||
@ -185,7 +197,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
||||
{
|
||||
Anchor = 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)
|
||||
},
|
||||
new FillFlowContainer
|
||||
@ -196,7 +208,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
||||
Origin = Anchor.CentreLeft,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ScheduleMatchDate(match.NewValue.Date.Value)
|
||||
new ScheduleMatchDate(currentMatch.Value.Date.Value)
|
||||
{
|
||||
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular)
|
||||
}
|
||||
@ -282,6 +294,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
||||
{
|
||||
Direction = FillDirection.Vertical,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(0, -6),
|
||||
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