mirror of
https://github.com/ppy/osu.git
synced 2026-06-06 06:23:41 +08:00
Compare commits
35 Commits
pp-dev
..
2026.605.0
+3
-3
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.EmptyFreeform
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
{
|
{
|
||||||
return new DifficultyAttributes(mods, 0);
|
return new DifficultyAttributes(mods, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>();
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>();
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.Pippidon
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
{
|
{
|
||||||
return new DifficultyAttributes(mods, 0);
|
return new DifficultyAttributes(mods, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>();
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>();
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.EmptyScrolling
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
{
|
{
|
||||||
return new DifficultyAttributes(mods, 0);
|
return new DifficultyAttributes(mods, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>();
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>();
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.Pippidon
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
{
|
{
|
||||||
return new DifficultyAttributes(mods, 0);
|
return new DifficultyAttributes(mods, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods) => Enumerable.Empty<DifficultyHitObject>();
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => Array.Empty<Skill>();
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.513.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.527.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Benchmarks
|
||||||
|
{
|
||||||
|
public class BenchmarkScoreMultiplierCalculator : BenchmarkTest
|
||||||
|
{
|
||||||
|
private ScoreMultiplierCalculator calculator = null!;
|
||||||
|
|
||||||
|
[Params(1, 10, 100)]
|
||||||
|
public int Times { get; set; }
|
||||||
|
|
||||||
|
public record ModTestCase(string Description, IEnumerable<Mod> Mods)
|
||||||
|
{
|
||||||
|
public override string ToString() => Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<ModTestCase> ValuesForMods =>
|
||||||
|
[
|
||||||
|
new ModTestCase("no mods", []),
|
||||||
|
new ModTestCase("single mod", [new OsuModHardRock()]),
|
||||||
|
new ModTestCase("single mod 2", [new OsuModEasy()]),
|
||||||
|
new ModTestCase("multiple mods", [new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime()]),
|
||||||
|
new ModTestCase("mods with adjusted settings", [
|
||||||
|
new OsuModDoubleTime { SpeedChange = { Value = 2 } },
|
||||||
|
new OsuModHidden { OnlyFadeApproachCircles = { Value = true } },
|
||||||
|
new OsuModHardRock()
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
[ParamsSource(nameof(ValuesForMods))]
|
||||||
|
public ModTestCase Mods { get; set; } = null!;
|
||||||
|
|
||||||
|
public override void SetUp()
|
||||||
|
{
|
||||||
|
base.SetUp();
|
||||||
|
calculator = new OsuRuleset().CreateScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public double ViaCalculator()
|
||||||
|
=> viaCalculator(Times, Mods);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ViaCalculator([Values(100)] int times, [ValueSource(nameof(ValuesForMods))] ModTestCase mods)
|
||||||
|
=> viaCalculator(times, mods);
|
||||||
|
|
||||||
|
private double viaCalculator(int times, ModTestCase mods)
|
||||||
|
{
|
||||||
|
double scoreMultiplier = 1;
|
||||||
|
|
||||||
|
for (int i = 0; i < times; ++i)
|
||||||
|
scoreMultiplier = calculator.CalculateFor(mods.Mods);
|
||||||
|
|
||||||
|
return scoreMultiplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch.Tests";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch.Tests";
|
||||||
|
|
||||||
[TestCase(4.039861734717169d, 127, "diffcalc-test")]
|
[TestCase(4.0505463516206195d, 127, "diffcalc-test")]
|
||||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||||
|
|
||||||
[TestCase(5.1527173897800873d, 127, "diffcalc-test")]
|
[TestCase(5.1696411260785498d, 127, "diffcalc-test")]
|
||||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new CatchModDoubleTime());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new CatchModDoubleTime());
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Catch.Mods;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Tests.Rulesets;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Tests
|
||||||
|
{
|
||||||
|
public class CatchScoreMultiplierTest : RulesetScoreMultiplierTest
|
||||||
|
{
|
||||||
|
public CatchScoreMultiplierTest()
|
||||||
|
: base(new CatchRuleset())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly object[][] test_cases =
|
||||||
|
[
|
||||||
|
#region Difficulty Reduction
|
||||||
|
|
||||||
|
[new Mod[] { new CatchModEasy() }, 0.5],
|
||||||
|
[new Mod[] { new CatchModNoFail() }, 0.5],
|
||||||
|
|
||||||
|
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.50 } } }, 0.1],
|
||||||
|
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.55 } } }, 0.1],
|
||||||
|
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.60 } } }, 0.2],
|
||||||
|
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.65 } } }, 0.2],
|
||||||
|
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.70 } } }, 0.3],
|
||||||
|
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.75 } } }, 0.3],
|
||||||
|
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.80 } } }, 0.4],
|
||||||
|
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.85 } } }, 0.4],
|
||||||
|
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.90 } } }, 0.5],
|
||||||
|
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.95 } } }, 0.5],
|
||||||
|
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.99 } } }, 0.5],
|
||||||
|
|
||||||
|
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.50 } } }, 0.1],
|
||||||
|
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.55 } } }, 0.1],
|
||||||
|
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.60 } } }, 0.2],
|
||||||
|
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.65 } } }, 0.2],
|
||||||
|
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.70 } } }, 0.3],
|
||||||
|
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.75 } } }, 0.3],
|
||||||
|
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.80 } } }, 0.4],
|
||||||
|
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.85 } } }, 0.4],
|
||||||
|
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.90 } } }, 0.5],
|
||||||
|
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.95 } } }, 0.5],
|
||||||
|
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.99 } } }, 0.5],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Difficulty Increase
|
||||||
|
|
||||||
|
[new Mod[] { new CatchModHardRock() }, 1.12],
|
||||||
|
[new Mod[] { new CatchModSuddenDeath() }, 1],
|
||||||
|
[new Mod[] { new CatchModPerfect() }, 1],
|
||||||
|
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.01 } } }, 1.00],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.05 } } }, 1.00],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.10 } } }, 1.02],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.15 } } }, 1.02],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.20 } } }, 1.04],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.25 } } }, 1.04],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.30 } } }, 1.06],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.35 } } }, 1.06],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.40 } } }, 1.08],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.45 } } }, 1.08],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.50 } } }, 1.10],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.55 } } }, 1.10],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.60 } } }, 1.12],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.65 } } }, 1.12],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.70 } } }, 1.14],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.75 } } }, 1.14],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.80 } } }, 1.16],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.85 } } }, 1.16],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.90 } } }, 1.18],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.95 } } }, 1.18],
|
||||||
|
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 2.00 } } }, 1.20],
|
||||||
|
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.01 } } }, 1.00],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.05 } } }, 1.00],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.10 } } }, 1.02],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.15 } } }, 1.02],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.20 } } }, 1.04],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.25 } } }, 1.04],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.30 } } }, 1.06],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.35 } } }, 1.06],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.40 } } }, 1.08],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.45 } } }, 1.08],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.50 } } }, 1.10],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.55 } } }, 1.10],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.60 } } }, 1.12],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.65 } } }, 1.12],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.70 } } }, 1.14],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.75 } } }, 1.14],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.80 } } }, 1.16],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.85 } } }, 1.16],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.90 } } }, 1.18],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.95 } } }, 1.18],
|
||||||
|
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 2.00 } } }, 1.20],
|
||||||
|
|
||||||
|
[new Mod[] { new CatchModHidden() }, 1.06],
|
||||||
|
|
||||||
|
[new Mod[] { new CatchModFlashlight() }, 1.12],
|
||||||
|
[new Mod[] { new CatchModFlashlight { ComboBasedSize = { Value = false } } }, 1],
|
||||||
|
|
||||||
|
[new Mod[] { new ModAccuracyChallenge() }, 1],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Conversion
|
||||||
|
|
||||||
|
[new Mod[] { new CatchModDifficultyAdjust() }, 0.5],
|
||||||
|
[new Mod[] { new CatchModClassic() }, 1],
|
||||||
|
[new Mod[] { new CatchModMirror() }, 1],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Automation
|
||||||
|
|
||||||
|
[new Mod[] { new CatchModAutoplay() }, 1],
|
||||||
|
[new Mod[] { new CatchModCinema() }, 1],
|
||||||
|
[new Mod[] { new CatchModRelax() }, 0.1],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Fun
|
||||||
|
|
||||||
|
[new Mod[] { new ModWindUp() }, 0.5],
|
||||||
|
[new Mod[] { new ModWindDown() }, 0.5],
|
||||||
|
[new Mod[] { new CatchModFloatingFruits() }, 1],
|
||||||
|
[new Mod[] { new CatchModMuted() }, 1],
|
||||||
|
[new Mod[] { new CatchModNoScope() }, 1],
|
||||||
|
[new Mod[] { new CatchModMovingFast() }, 1],
|
||||||
|
[new Mod[] { new CatchModSynesthesia() }, 0.8],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region System
|
||||||
|
|
||||||
|
[new Mod[] { new ModScoreV2() }, 1],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Combinations
|
||||||
|
|
||||||
|
[new Mod[] { new CatchModHidden(), new CatchModHardRock() }, 1.06 * 1.12]
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
];
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(test_cases))]
|
||||||
|
public void TestMultipliers(Mod[] mods, double expectedMultiplier)
|
||||||
|
=> TestModCombination(mods, expectedMultiplier);
|
||||||
|
|
||||||
|
[TestCase(30000001, 0.96)]
|
||||||
|
[TestCase(30000009, 0.96)]
|
||||||
|
[TestCase(30000016, 0.96)]
|
||||||
|
[TestCase(30000017, 1)]
|
||||||
|
[TestCase(null, 1)]
|
||||||
|
public void TestClassicMultiplierVersioning(int? totalScoreVersion, double expectedMultiplier)
|
||||||
|
{
|
||||||
|
var scoreInfo = totalScoreVersion != null ? new ScoreInfo { TotalScoreVersion = totalScoreVersion.Value } : null;
|
||||||
|
var calculator = Ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty(), scoreInfo));
|
||||||
|
Assert.That(calculator.CalculateFor([new CatchModClassic()]), Is.EqualTo(expectedMultiplier).Within(Precision.DOUBLE_EPSILON));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -169,6 +169,8 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new CatchScoreMultiplierCalculator(context);
|
||||||
|
|
||||||
public override string Description => "osu!catch";
|
public override string Description => "osu!catch";
|
||||||
|
|
||||||
public override string ShortName => SHORT_NAME;
|
public override string ShortName => SHORT_NAME;
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ using osu.Game.Rulesets.Difficulty;
|
|||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Utils;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||||
{
|
{
|
||||||
@@ -23,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
{
|
{
|
||||||
private const double difficulty_multiplier = 4.59;
|
private const double difficulty_multiplier = 4.59;
|
||||||
|
|
||||||
|
private float halfCatcherWidth;
|
||||||
|
|
||||||
public override int Version => 20251020;
|
public override int Version => 20251020;
|
||||||
|
|
||||||
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
@@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
{
|
{
|
||||||
if (beatmap.HitObjects.Count == 0)
|
if (beatmap.HitObjects.Count == 0)
|
||||||
return new CatchDifficultyAttributes { Mods = mods };
|
return new CatchDifficultyAttributes { Mods = mods };
|
||||||
@@ -45,19 +46,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods)
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
{
|
{
|
||||||
CatchHitObject? lastObject = null;
|
CatchHitObject? lastObject = null;
|
||||||
|
|
||||||
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
||||||
|
|
||||||
double clockRate = ModUtils.CalculateRateWithMods(mods);
|
|
||||||
|
|
||||||
float halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f;
|
|
||||||
|
|
||||||
// For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay.
|
|
||||||
halfCatcherWidth *= 1 - (Math.Max(0, beatmap.Difficulty.CircleSize - 5.5f) * 0.0625f);
|
|
||||||
|
|
||||||
// In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream.
|
// In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream.
|
||||||
foreach (var hitObject in CatchBeatmap.GetPalpableObjects(beatmap.HitObjects))
|
foreach (var hitObject in CatchBeatmap.GetPalpableObjects(beatmap.HitObjects))
|
||||||
{
|
{
|
||||||
@@ -74,11 +68,16 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
return objects;
|
return objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||||
{
|
{
|
||||||
|
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f;
|
||||||
|
|
||||||
|
// For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay.
|
||||||
|
halfCatcherWidth *= 1 - (Math.Max(0, beatmap.Difficulty.CircleSize - 5.5f) * 0.0625f);
|
||||||
|
|
||||||
return new Skill[]
|
return new Skill[]
|
||||||
{
|
{
|
||||||
new Movement(mods),
|
new Movement(mods, halfCatcherWidth, clockRate),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,16 +11,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Evaluators
|
|||||||
{
|
{
|
||||||
private const double direction_change_bonus = 21.0;
|
private const double direction_change_bonus = 21.0;
|
||||||
|
|
||||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
public static double EvaluateDifficultyOf(DifficultyHitObject current, double catcherSpeedMultiplier)
|
||||||
{
|
{
|
||||||
var catchCurrent = (CatchDifficultyHitObject)current;
|
var catchCurrent = (CatchDifficultyHitObject)current;
|
||||||
var catchLast = (CatchDifficultyHitObject)current.Previous(0);
|
var catchLast = (CatchDifficultyHitObject)current.Previous(0);
|
||||||
var catchLastLast = (CatchDifficultyHitObject)current.Previous(1);
|
var catchLastLast = (CatchDifficultyHitObject)current.Previous(1);
|
||||||
|
|
||||||
// In catch, clockrate adjustments do not only affect the timings of hitobjects,
|
|
||||||
// but also the speed of the player's catcher, which has an impact on difficulty
|
|
||||||
double catcherSpeedMultiplier = current.ClockRate;
|
|
||||||
|
|
||||||
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
|
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
|
||||||
|
|
||||||
double distanceAddition = (Math.Pow(Math.Abs(catchCurrent.DistanceMoved), 1.3) / 510);
|
double distanceAddition = (Math.Pow(Math.Abs(catchCurrent.DistanceMoved), 1.3) / 510);
|
||||||
@@ -44,30 +40,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Evaluators
|
|||||||
/ (CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 6) / sqrtStrain;
|
/ (CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 6) / sqrtStrain;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linear spacing nerf.
|
|
||||||
double linearSpacingCount = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < Math.Min(current.Index, 10); i++)
|
|
||||||
{
|
|
||||||
var catchPrevObj = (CatchDifficultyHitObject)catchCurrent.Previous(i);
|
|
||||||
|
|
||||||
// Only same direction movements matter as they do not take any additional inputs.
|
|
||||||
if (Math.Sign(catchCurrent.DistanceMoved) != Math.Sign(catchPrevObj.DistanceMoved) || catchCurrent.DistanceMoved == 0 || catchPrevObj.DistanceMoved == 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
double currentSpacing = Math.Abs(catchCurrent.DistanceMoved / catchCurrent.StrainTime);
|
|
||||||
double prevSpacing = Math.Abs(catchPrevObj.DistanceMoved / catchPrevObj.StrainTime);
|
|
||||||
|
|
||||||
double relativeDifference = Math.Abs(currentSpacing / prevSpacing - 1);
|
|
||||||
|
|
||||||
if (relativeDifference > 0.05)
|
|
||||||
break;
|
|
||||||
|
|
||||||
linearSpacingCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
distanceAddition *= Math.Pow(0.7, linearSpacingCount);
|
|
||||||
|
|
||||||
// Bonus for edge dashes.
|
// Bonus for edge dashes.
|
||||||
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
|
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,14 +17,28 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
|||||||
|
|
||||||
protected override int SectionLength => 750;
|
protected override int SectionLength => 750;
|
||||||
|
|
||||||
public Movement(Mod[] mods)
|
protected readonly float HalfCatcherWidth;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The speed multiplier applied to the player's catcher.
|
||||||
|
/// </summary>
|
||||||
|
private readonly double catcherSpeedMultiplier;
|
||||||
|
|
||||||
|
public Movement(Mod[] mods, float halfCatcherWidth, double clockRate)
|
||||||
: base(mods)
|
: base(mods)
|
||||||
{
|
{
|
||||||
|
HalfCatcherWidth = halfCatcherWidth;
|
||||||
|
|
||||||
|
// In catch, clockrate adjustments do not only affect the timings of hitobjects,
|
||||||
|
// but also the speed of the player's catcher, which has an impact on difficulty
|
||||||
|
// TODO: Support variable clockrates caused by mods such as ModTimeRamp
|
||||||
|
// (perhaps by using IApplicableToRate within the CatchDifficultyHitObject constructor to set a catcher speed for each object before processing)
|
||||||
|
catcherSpeedMultiplier = clockRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override double StrainValueOf(DifficultyHitObject current)
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
return MovementEvaluator.EvaluateDifficultyOf(current);
|
return MovementEvaluator.EvaluateDifficultyOf(current, catcherSpeedMultiplier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Edit.Tools;
|
using osu.Game.Rulesets.Edit.Tools;
|
||||||
|
|
||||||
@@ -13,11 +13,11 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
public class BananaShowerCompositionTool : CompositionTool
|
public class BananaShowerCompositionTool : CompositionTool
|
||||||
{
|
{
|
||||||
public BananaShowerCompositionTool()
|
public BananaShowerCompositionTool()
|
||||||
: base(nameof(BananaShower))
|
: base("Banana shower")
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
|
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorBananaShower };
|
||||||
|
|
||||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
|
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,16 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@@ -22,6 +27,29 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override Drawable CreateNewComboButton() => new NewComboTernaryButton
|
||||||
|
{
|
||||||
|
Current = NewCombo,
|
||||||
|
CreateIcon = () => new Container
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new SpriteIcon
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.BottomLeft,
|
||||||
|
Icon = OsuIcon.EditorFruit,
|
||||||
|
Size = new Vector2(15),
|
||||||
|
},
|
||||||
|
new SpriteIcon
|
||||||
|
{
|
||||||
|
Icon = OsuIcon.EditorNewComboSparkles,
|
||||||
|
Size = new Vector2(20),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new CatchSelectionHandler();
|
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new CatchSelectionHandler();
|
||||||
|
|
||||||
public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject)
|
public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject)
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorFruit };
|
||||||
|
|
||||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
|
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Edit.Tools;
|
using osu.Game.Rulesets.Edit.Tools;
|
||||||
|
|
||||||
@@ -13,11 +13,11 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
public class JuiceStreamCompositionTool : CompositionTool
|
public class JuiceStreamCompositionTool : CompositionTool
|
||||||
{
|
{
|
||||||
public JuiceStreamCompositionTool()
|
public JuiceStreamCompositionTool()
|
||||||
: base(nameof(JuiceStream))
|
: base("Juice stream")
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorJuiceStream };
|
||||||
|
|
||||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint();
|
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ namespace osu.Game.Rulesets.Catch.Mods
|
|||||||
{
|
{
|
||||||
public partial class CatchModFlashlight : ModFlashlight<CatchHitObject>
|
public partial class CatchModFlashlight : ModFlashlight<CatchHitObject>
|
||||||
{
|
{
|
||||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
|
|
||||||
|
|
||||||
public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
|
public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
|
||||||
{
|
{
|
||||||
MinValue = 0.5f,
|
MinValue = 0.5f,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Catch.Mods
|
|||||||
public override string Name => "Floating Fruits";
|
public override string Name => "Floating Fruits";
|
||||||
public override string Acronym => "FF";
|
public override string Acronym => "FF";
|
||||||
public override LocalisableString Description => "The fruits are... floating?";
|
public override LocalisableString Description => "The fruits are... floating?";
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
public override IconUsage? Icon => OsuIcon.ModFloatingFruits;
|
public override IconUsage? Icon => OsuIcon.ModFloatingFruits;
|
||||||
|
|
||||||
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ namespace osu.Game.Rulesets.Catch.Mods
|
|||||||
{
|
{
|
||||||
public class CatchModHardRock : ModHardRock, IApplicableToBeatmapProcessor
|
public class CatchModHardRock : ModHardRock, IApplicableToBeatmapProcessor
|
||||||
{
|
{
|
||||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
|
|
||||||
|
|
||||||
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
|
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
|
||||||
{
|
{
|
||||||
var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
|
var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Catch.Mods
|
|||||||
public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset<CatchHitObject>
|
public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset<CatchHitObject>
|
||||||
{
|
{
|
||||||
public override LocalisableString Description => @"Play with fading fruits.";
|
public override LocalisableString Description => @"Play with fading fruits.";
|
||||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
|
|
||||||
|
|
||||||
private const double fade_out_offset_multiplier = 0.6;
|
private const double fade_out_offset_multiplier = 0.6;
|
||||||
private const double fade_out_duration_multiplier = 0.44;
|
private const double fade_out_duration_multiplier = 0.44;
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ namespace osu.Game.Rulesets.Catch.Mods
|
|||||||
public override string Acronym => "MF";
|
public override string Acronym => "MF";
|
||||||
public override LocalisableString Description => "Dashing by default, slow down!";
|
public override LocalisableString Description => "Dashing by default, slow down!";
|
||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
public override IconUsage? Icon => OsuIcon.ModMovingFast;
|
public override IconUsage? Icon => OsuIcon.ModMovingFast;
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) };
|
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) };
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// 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.Catch.Mods;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Scoring
|
||||||
|
{
|
||||||
|
public class CatchScoreMultiplierCalculator : ScoreMultiplierCalculator
|
||||||
|
{
|
||||||
|
public CatchScoreMultiplierCalculator(ScoreMultiplierContext context)
|
||||||
|
: base(context)
|
||||||
|
{
|
||||||
|
#region Difficulty Reduction
|
||||||
|
|
||||||
|
Single<CatchModEasy>(hasMultiplier: 0.5);
|
||||||
|
Single<CatchModNoFail>(hasMultiplier: 0.5);
|
||||||
|
Single<CatchModHalfTime>(hasMultiplier: halfTime => rateAdjustMultiplier(halfTime.SpeedChange.Value));
|
||||||
|
Single<CatchModDaycore>(hasMultiplier: daycore => rateAdjustMultiplier(daycore.SpeedChange.Value));
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Difficulty Increase
|
||||||
|
|
||||||
|
Single<CatchModHardRock>(hasMultiplier: hardRock => hardRock.UsesDefaultConfiguration ? 1.12 : 1);
|
||||||
|
// Sudden Death
|
||||||
|
// Perfect
|
||||||
|
Single<CatchModDoubleTime>(hasMultiplier: doubleTime => rateAdjustMultiplier(doubleTime.SpeedChange.Value));
|
||||||
|
Single<CatchModNightcore>(hasMultiplier: nightcore => rateAdjustMultiplier(nightcore.SpeedChange.Value));
|
||||||
|
Single<CatchModHidden>(hasMultiplier: hidden => hidden.UsesDefaultConfiguration ? 1.06 : 1);
|
||||||
|
Single<CatchModFlashlight>(hasMultiplier: flashlight => flashlight.UsesDefaultConfiguration ? 1.12 : 1);
|
||||||
|
// Accuracy Challenge
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Conversion
|
||||||
|
|
||||||
|
Single<CatchModDifficultyAdjust>(hasMultiplier: 0.5);
|
||||||
|
Single<CatchModClassic>(hasMultiplier: _ => classicMultiplier(context.Score));
|
||||||
|
// Mirror
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Automation
|
||||||
|
|
||||||
|
// Autoplay
|
||||||
|
// Cinema
|
||||||
|
Single<CatchModRelax>(hasMultiplier: 0.1);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Fun
|
||||||
|
|
||||||
|
Single<ModWindUp>(hasMultiplier: 0.5);
|
||||||
|
Single<ModWindDown>(hasMultiplier: 0.5);
|
||||||
|
// Floating Fruits
|
||||||
|
// Muted
|
||||||
|
// No Scope
|
||||||
|
// Moving Fast
|
||||||
|
Single<CatchModSynesthesia>(hasMultiplier: 0.8);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region System
|
||||||
|
|
||||||
|
// Score V2
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double rateAdjustMultiplier(double speedChange)
|
||||||
|
{
|
||||||
|
// Round to the nearest multiple of 0.1.
|
||||||
|
double value = (int)(speedChange * 10) / 10.0;
|
||||||
|
|
||||||
|
// Offset back to 0.
|
||||||
|
value -= 1;
|
||||||
|
|
||||||
|
if (speedChange >= 1)
|
||||||
|
return 1 + value / 5;
|
||||||
|
else
|
||||||
|
return 0.6 + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double classicMultiplier(ScoreInfo? score)
|
||||||
|
{
|
||||||
|
if (score != null && score.TotalScoreVersion < 30000017)
|
||||||
|
return 0.96;
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
var decoded = DecodeFromLegacy(beatmaps_resource_store.GetStream($"Resources/Testing/Beatmaps/{name}.osu"), beatmaps_resource_store, name);
|
var decoded = DecodeFromLegacy(beatmaps_resource_store.GetStream($"Resources/Testing/Beatmaps/{name}.osu"), beatmaps_resource_store, name);
|
||||||
var decodedAfterEncode = DecodeFromLegacy(EncodeToLegacy(decoded), beatmaps_resource_store, name);
|
var decodedAfterEncode = DecodeFromLegacy(EncodeToLegacy(decoded), beatmaps_resource_store, name);
|
||||||
|
|
||||||
Sort(decoded.beatmap);
|
Sort(decoded.Beatmap);
|
||||||
Sort(decodedAfterEncode.beatmap);
|
Sort(decodedAfterEncode.Beatmap);
|
||||||
|
|
||||||
CompareBeatmaps(decoded, decodedAfterEncode);
|
CompareBeatmaps(decoded, decodedAfterEncode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
// 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.Utils;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Mods;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Tests.Rulesets;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Tests
|
||||||
|
{
|
||||||
|
public class ManiaScoreMultiplierTest : RulesetScoreMultiplierTest
|
||||||
|
{
|
||||||
|
public ManiaScoreMultiplierTest()
|
||||||
|
: base(new ManiaRuleset())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly object[][] test_cases =
|
||||||
|
[
|
||||||
|
#region Difficulty Reduction
|
||||||
|
|
||||||
|
[new Mod[] { new ManiaModEasy() }, 0.5],
|
||||||
|
[new Mod[] { new ManiaModNoFail() }, 0.5],
|
||||||
|
|
||||||
|
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.50 } } }, 0.1],
|
||||||
|
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.55 } } }, 0.1],
|
||||||
|
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.60 } } }, 0.2],
|
||||||
|
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.65 } } }, 0.2],
|
||||||
|
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.70 } } }, 0.3],
|
||||||
|
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.75 } } }, 0.3],
|
||||||
|
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.80 } } }, 0.4],
|
||||||
|
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.85 } } }, 0.4],
|
||||||
|
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.90 } } }, 0.5],
|
||||||
|
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.95 } } }, 0.5],
|
||||||
|
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.99 } } }, 0.5],
|
||||||
|
|
||||||
|
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.50 } } }, 0.1],
|
||||||
|
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.55 } } }, 0.1],
|
||||||
|
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.60 } } }, 0.2],
|
||||||
|
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.65 } } }, 0.2],
|
||||||
|
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.70 } } }, 0.3],
|
||||||
|
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.75 } } }, 0.3],
|
||||||
|
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.80 } } }, 0.4],
|
||||||
|
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.85 } } }, 0.4],
|
||||||
|
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.90 } } }, 0.5],
|
||||||
|
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.95 } } }, 0.5],
|
||||||
|
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.99 } } }, 0.5],
|
||||||
|
|
||||||
|
[new Mod[] { new ManiaModNoRelease() }, 0.9],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Difficulty Increase
|
||||||
|
|
||||||
|
[new Mod[] { new ManiaModHardRock() }, 1],
|
||||||
|
[new Mod[] { new ManiaModSuddenDeath() }, 1],
|
||||||
|
[new Mod[] { new ManiaModPerfect() }, 1],
|
||||||
|
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.01 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.05 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.10 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.15 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.20 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.25 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.30 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.35 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.40 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.45 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.50 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.55 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.60 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.65 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.70 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.75 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.80 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.85 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.90 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.95 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 2.00 } } }, 1],
|
||||||
|
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.01 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.05 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.10 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.15 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.20 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.25 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.30 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.35 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.40 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.45 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.50 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.55 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.60 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.65 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.70 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.75 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.80 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.85 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.90 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.95 } } }, 1],
|
||||||
|
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 2.00 } } }, 1],
|
||||||
|
|
||||||
|
[new Mod[] { new ManiaModFadeIn() }, 1],
|
||||||
|
[new Mod[] { new ManiaModHidden() }, 1],
|
||||||
|
[new Mod[] { new ManiaModCover() }, 1],
|
||||||
|
|
||||||
|
[new Mod[] { new ManiaModFlashlight() }, 1],
|
||||||
|
[new Mod[] { new ModAccuracyChallenge() }, 1],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Conversion
|
||||||
|
|
||||||
|
[new Mod[] { new ManiaModRandom() }, 1],
|
||||||
|
[new Mod[] { new ManiaModDualStages() }, 1],
|
||||||
|
[new Mod[] { new ManiaModMirror() }, 1],
|
||||||
|
[new Mod[] { new ManiaModDifficultyAdjust() }, 0.5],
|
||||||
|
[new Mod[] { new ManiaModClassic() }, 1],
|
||||||
|
[new Mod[] { new ManiaModInvert() }, 1],
|
||||||
|
[new Mod[] { new ManiaModConstantSpeed() }, 0.9],
|
||||||
|
[new Mod[] { new ManiaModHoldOff() }, 0.9],
|
||||||
|
[new Mod[] { new ManiaModKey1() }, 0.9],
|
||||||
|
[new Mod[] { new ManiaModKey2() }, 0.9],
|
||||||
|
[new Mod[] { new ManiaModKey3() }, 0.9],
|
||||||
|
[new Mod[] { new ManiaModKey4() }, 0.9],
|
||||||
|
[new Mod[] { new ManiaModKey5() }, 0.9],
|
||||||
|
[new Mod[] { new ManiaModKey6() }, 0.9],
|
||||||
|
[new Mod[] { new ManiaModKey7() }, 0.9],
|
||||||
|
[new Mod[] { new ManiaModKey8() }, 0.9],
|
||||||
|
[new Mod[] { new ManiaModKey9() }, 0.9],
|
||||||
|
[new Mod[] { new ManiaModKey10() }, 0.9],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Automation
|
||||||
|
|
||||||
|
[new Mod[] { new ManiaModAutoplay() }, 1],
|
||||||
|
[new Mod[] { new ManiaModCinema() }, 1],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Fun
|
||||||
|
|
||||||
|
[new Mod[] { new ModWindUp() }, 0.5],
|
||||||
|
[new Mod[] { new ModWindDown() }, 0.5],
|
||||||
|
[new Mod[] { new ManiaModMuted() }, 1],
|
||||||
|
[new Mod[] { new ModAdaptiveSpeed() }, 0.5],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region System
|
||||||
|
|
||||||
|
[new Mod[] { new ManiaModScoreV2() }, 1],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Combinations
|
||||||
|
|
||||||
|
[new Mod[] { new ManiaModEasy(), new ManiaModKey4() }, 0.5 * 0.9]
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
];
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(test_cases))]
|
||||||
|
public void TestMultipliers(Mod[] mods, double expectedMultiplier)
|
||||||
|
=> TestModCombination(mods, expectedMultiplier);
|
||||||
|
|
||||||
|
private static readonly object[][] key_mod_multiplier_test_cases =
|
||||||
|
[
|
||||||
|
// score end date, client version, expected multiplier
|
||||||
|
|
||||||
|
// scores verifiably from old clients.
|
||||||
|
[new DateTimeOffset(2024, 1, 31, 11, 0, 0, TimeSpan.Zero), "2024.130.2", 1],
|
||||||
|
[new DateTimeOffset(2024, 12, 9, 11, 0, 0, TimeSpan.Zero), "2024.1208.0", 1],
|
||||||
|
[new DateTimeOffset(2025, 6, 12, 11, 0, 0, TimeSpan.Zero), "2025.605.3", 1],
|
||||||
|
[new DateTimeOffset(2025, 6, 28, 11, 0, 0, TimeSpan.Zero), "2025.625.0-tachyon", 1],
|
||||||
|
[new DateTimeOffset(2025, 7, 11, 11, 0, 0, TimeSpan.Zero), "2025.710.0-lazer", 1],
|
||||||
|
[new DateTimeOffset(2025, 7, 15, 11, 0, 0, TimeSpan.Zero), "2025.711.0-tachyon", 1],
|
||||||
|
|
||||||
|
// scores without explicit client versions, predating the change of multiplier.
|
||||||
|
// those MUST have used the old multiplier.
|
||||||
|
[new DateTimeOffset(2024, 1, 31, 11, 0, 0, TimeSpan.Zero), "", 1],
|
||||||
|
[new DateTimeOffset(2024, 12, 9, 11, 0, 0, TimeSpan.Zero), "", 1],
|
||||||
|
[new DateTimeOffset(2025, 6, 12, 11, 0, 0, TimeSpan.Zero), "", 1],
|
||||||
|
[new DateTimeOffset(2025, 6, 28, 11, 0, 0, TimeSpan.Zero), "", 1],
|
||||||
|
[new DateTimeOffset(2025, 7, 11, 11, 0, 0, TimeSpan.Zero), "", 1],
|
||||||
|
[new DateTimeOffset(2025, 7, 15, 11, 0, 0, TimeSpan.Zero), "", 1],
|
||||||
|
|
||||||
|
// scores without explicit client versions, AFTER the change of multiplier.
|
||||||
|
// there is NO way of verifying whether these scores use the new or old multiplier, therefore GUESS that it's the new one.
|
||||||
|
// "thankfully" the window of opportunity for this occurring *should* be slim
|
||||||
|
// (from client release with new key mod multipliers on July 18, 2025
|
||||||
|
// until spectator server release which added client version writing to server-side replays on August 1, 2025).
|
||||||
|
[new DateTimeOffset(2025, 7, 19, 0, 20, 15, 0, TimeSpan.Zero), "", 0.9],
|
||||||
|
[new DateTimeOffset(2025, 7, 23, 0, 20, 15, 0, TimeSpan.Zero), "", 0.9],
|
||||||
|
[new DateTimeOffset(2025, 8, 19, 0, 20, 15, 0, TimeSpan.Zero), "", 0.9],
|
||||||
|
[new DateTimeOffset(2026, 6, 18, 0, 20, 15, 0, TimeSpan.Zero), "", 0.9],
|
||||||
|
[new DateTimeOffset(2026, 7, 18, 0, 20, 15, 0, TimeSpan.Zero), "", 0.9],
|
||||||
|
|
||||||
|
// scores verifiably from new clients.
|
||||||
|
[new DateTimeOffset(2025, 7, 19, 0, 20, 15, 0, TimeSpan.Zero), "2025.718.0-tachyon", 0.9],
|
||||||
|
[new DateTimeOffset(2025, 7, 23, 0, 20, 15, 0, TimeSpan.Zero), "2025.721.0-tachyon", 0.9],
|
||||||
|
[new DateTimeOffset(2025, 8, 19, 0, 20, 15, 0, TimeSpan.Zero), "2025.816.0-lazer", 0.9],
|
||||||
|
[new DateTimeOffset(2026, 6, 18, 0, 20, 15, 0, TimeSpan.Zero), "2026.518.0-lazer", 0.9],
|
||||||
|
[new DateTimeOffset(2026, 7, 18, 0, 20, 15, 0, TimeSpan.Zero), "2026.522.1-tachyon", 0.9],
|
||||||
|
];
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(key_mod_multiplier_test_cases))]
|
||||||
|
public void TestKeyModMultiplierCompatibility(DateTimeOffset endDate, string clientVersion, double expectedMultiplier)
|
||||||
|
{
|
||||||
|
var calculator = Ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty(), new ScoreInfo
|
||||||
|
{
|
||||||
|
Date = endDate,
|
||||||
|
ClientVersion = clientVersion
|
||||||
|
}));
|
||||||
|
Assert.That(calculator.CalculateFor([new ManiaModKey4()]), Is.EqualTo(expectedMultiplier).Within(Precision.DOUBLE_EPSILON));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(30000001, 0.96)]
|
||||||
|
[TestCase(30000009, 0.96)]
|
||||||
|
[TestCase(30000016, 0.96)]
|
||||||
|
[TestCase(30000017, 1)]
|
||||||
|
[TestCase(null, 1)]
|
||||||
|
public void TestClassicMultiplierVersioning(int? totalScoreVersion, double expectedMultiplier)
|
||||||
|
{
|
||||||
|
var scoreInfo = totalScoreVersion != null ? new ScoreInfo { TotalScoreVersion = totalScoreVersion.Value } : null;
|
||||||
|
var calculator = Ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty(), scoreInfo));
|
||||||
|
Assert.That(calculator.CalculateFor([new ManiaModClassic()]), Is.EqualTo(expectedMultiplier).Within(Precision.DOUBLE_EPSILON));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,10 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Rulesets.Mania.Mods;
|
using osu.Game.Rulesets.Mania.Mods;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osu.Game.Rulesets.Mania.Replays;
|
using osu.Game.Rulesets.Mania.Replays;
|
||||||
|
using osu.Game.Rulesets.Mania.Scoring;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Tests.Mods
|
namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||||
@@ -54,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
|||||||
Mod = doubleTime,
|
Mod = doubleTime,
|
||||||
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0
|
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0
|
||||||
&& Player.ScoreProcessor.Accuracy.Value == 1
|
&& Player.ScoreProcessor.Accuracy.Value == 1
|
||||||
&& Player.ScoreProcessor.TotalScore.Value == (long)(1_000_000 * doubleTime.ScoreMultiplier),
|
&& Player.ScoreProcessor.TotalScore.Value == (long)(1_000_000 * new ManiaScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty())).CalculateFor([doubleTime])),
|
||||||
Autoplay = false,
|
Autoplay = false,
|
||||||
CreateBeatmap = () => new Beatmap
|
CreateBeatmap = () => new Beatmap
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ using osu.Game.Rulesets.Mania.Scoring;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Utils;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Difficulty
|
namespace osu.Game.Rulesets.Mania.Difficulty
|
||||||
{
|
{
|
||||||
@@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
|
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
{
|
{
|
||||||
if (beatmap.HitObjects.Count == 0)
|
if (beatmap.HitObjects.Count == 0)
|
||||||
return new ManiaDifficultyAttributes { Mods = mods };
|
return new ManiaDifficultyAttributes { Mods = mods };
|
||||||
@@ -63,13 +62,11 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods)
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
{
|
{
|
||||||
var sortedObjects = beatmap.HitObjects.ToArray();
|
var sortedObjects = beatmap.HitObjects.ToArray();
|
||||||
int totalColumns = ((ManiaBeatmap)beatmap).TotalColumns;
|
int totalColumns = ((ManiaBeatmap)beatmap).TotalColumns;
|
||||||
|
|
||||||
double clockRate = ModUtils.CalculateRateWithMods(mods);
|
|
||||||
|
|
||||||
LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
|
LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
|
||||||
|
|
||||||
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
||||||
@@ -91,7 +88,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
// Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
|
// Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
|
||||||
protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input;
|
protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input;
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
|
||||||
{
|
{
|
||||||
new Strain(mods, ((ManiaBeatmap)Beatmap).TotalColumns)
|
new Strain(mods, ((ManiaBeatmap)Beatmap).TotalColumns)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ using osu.Game.Rulesets.Edit;
|
|||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||||
using osu.Game.Rulesets.Mania.UI;
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
@@ -90,5 +92,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
|||||||
private float getNoteHeight(Column resultPlayfield) =>
|
private float getNoteHeight(Column resultPlayfield) =>
|
||||||
resultPlayfield.ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
|
resultPlayfield.ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
|
||||||
resultPlayfield.ToScreenSpace(Vector2.Zero).Y;
|
resultPlayfield.ToScreenSpace(Vector2.Zero).Y;
|
||||||
|
|
||||||
|
public override bool ReplacesExistingObject(HitObject existing)
|
||||||
|
=> base.ReplacesExistingObject(existing) && HitObject.Column == ((IHasColumn)existing).Column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Edit.Tools;
|
using osu.Game.Rulesets.Edit.Tools;
|
||||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||||
@@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorHoldNote };
|
||||||
|
|
||||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint();
|
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,16 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@@ -22,6 +27,29 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override Drawable CreateNewComboButton() => new NewComboTernaryButton
|
||||||
|
{
|
||||||
|
Current = NewCombo,
|
||||||
|
CreateIcon = () => new Container
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new SpriteIcon
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.BottomLeft,
|
||||||
|
Icon = OsuIcon.EditorNote,
|
||||||
|
Size = new Vector2(15),
|
||||||
|
},
|
||||||
|
new SpriteIcon
|
||||||
|
{
|
||||||
|
Icon = OsuIcon.EditorNewComboSparkles,
|
||||||
|
Size = new Vector2(20),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject)
|
public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject)
|
||||||
{
|
{
|
||||||
switch (hitObject)
|
switch (hitObject)
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Edit.Tools;
|
using osu.Game.Rulesets.Edit.Tools;
|
||||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||||
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorNote };
|
||||||
|
|
||||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint();
|
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,6 +307,8 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new ManiaScoreMultiplierCalculator(context);
|
||||||
|
|
||||||
public override string Description => "osu!mania";
|
public override string Description => "osu!mania";
|
||||||
|
|
||||||
public override string ShortName => SHORT_NAME;
|
public override string ShortName => SHORT_NAME;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
public override string Acronym => Name;
|
public override string Acronym => Name;
|
||||||
public abstract int KeyCount { get; }
|
public abstract int KeyCount { get; }
|
||||||
public override ModType Type => ModType.Conversion;
|
public override ModType Type => ModType.Conversion;
|
||||||
public override double ScoreMultiplier => 0.9;
|
|
||||||
public override bool Ranked => UsesDefaultConfiguration;
|
public override bool Ranked => UsesDefaultConfiguration;
|
||||||
|
|
||||||
public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
|
public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
|
|
||||||
public override string Acronym => "CS";
|
public override string Acronym => "CS";
|
||||||
|
|
||||||
public override double ScoreMultiplier => 0.9;
|
|
||||||
|
|
||||||
public override LocalisableString Description => "No more tricky speed changes!";
|
public override LocalisableString Description => "No more tricky speed changes!";
|
||||||
|
|
||||||
public override IconUsage? Icon => OsuIcon.ModConstantSpeed;
|
public override IconUsage? Icon => OsuIcon.ModConstantSpeed;
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
|
|
||||||
public override LocalisableString Description => @"Decrease the playfield's viewing area.";
|
public override LocalisableString Description => @"Decrease the playfield's viewing area.";
|
||||||
|
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
|
|
||||||
protected override CoverExpandDirection ExpandDirection => Direction.Value;
|
protected override CoverExpandDirection ExpandDirection => Direction.Value;
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
||||||
|
|||||||
@@ -7,9 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
{
|
{
|
||||||
public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod
|
public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod
|
||||||
{
|
{
|
||||||
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
|
|
||||||
// make the map harder and is more of a personal preference.
|
|
||||||
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
|
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
public override LocalisableString Description => @"Double the stages, double the fun!";
|
public override LocalisableString Description => @"Double the stages, double the fun!";
|
||||||
public override IconUsage? Icon => OsuIcon.ModDualStages;
|
public override IconUsage? Icon => OsuIcon.ModDualStages;
|
||||||
public override ModType Type => ModType.Conversion;
|
public override ModType Type => ModType.Conversion;
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
|
|
||||||
private bool isForCurrentRuleset;
|
private bool isForCurrentRuleset;
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
public override string Acronym => "FI";
|
public override string Acronym => "FI";
|
||||||
public override IconUsage? Icon => OsuIcon.ModFadeIn;
|
public override IconUsage? Icon => OsuIcon.ModFadeIn;
|
||||||
public override LocalisableString Description => @"Keys appear out of nowhere!";
|
public override LocalisableString Description => @"Keys appear out of nowhere!";
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
public override bool ValidForFreestyleAsRequiredMod => false;
|
public override bool ValidForFreestyleAsRequiredMod => false;
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
{
|
{
|
||||||
public partial class ManiaModFlashlight : ModFlashlight<ManiaHitObject>
|
public partial class ManiaModFlashlight : ModFlashlight<ManiaHitObject>
|
||||||
{
|
{
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(ModHidden) };
|
public override Type[] IncompatibleMods => new[] { typeof(ModHidden) };
|
||||||
|
|
||||||
public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
|
public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
{
|
{
|
||||||
public class ManiaModHardRock : ModHardRock, IApplicableToHitObject
|
public class ManiaModHardRock : ModHardRock, IApplicableToHitObject
|
||||||
{
|
{
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
public override bool Ranked => false;
|
public override bool Ranked => false;
|
||||||
|
|
||||||
public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1.4;
|
public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1.4;
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
private const float coverage_increase_per_combo = 0.5f;
|
private const float coverage_increase_per_combo = 0.5f;
|
||||||
|
|
||||||
public override LocalisableString Description => @"Keys fade out before you hit them!";
|
public override LocalisableString Description => @"Keys fade out before you hit them!";
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
|
|
||||||
public override string Acronym => "HO";
|
public override string Acronym => "HO";
|
||||||
|
|
||||||
public override double ScoreMultiplier => 0.9;
|
|
||||||
|
|
||||||
public override LocalisableString Description => @"Replaces all hold notes with normal notes.";
|
public override LocalisableString Description => @"Replaces all hold notes with normal notes.";
|
||||||
|
|
||||||
public override IconUsage? Icon => OsuIcon.ModHoldOff;
|
public override IconUsage? Icon => OsuIcon.ModHoldOff;
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
public override string Name => "Invert";
|
public override string Name => "Invert";
|
||||||
|
|
||||||
public override string Acronym => "IN";
|
public override string Acronym => "IN";
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
|
|
||||||
public override LocalisableString Description => "Hold the keys. To the beat.";
|
public override LocalisableString Description => "Hold the keys. To the beat.";
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,5 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
{
|
{
|
||||||
public class ManiaModNightcore : ModNightcore<ManiaHitObject>, IManiaRateAdjustmentMod
|
public class ManiaModNightcore : ModNightcore<ManiaHitObject>, IManiaRateAdjustmentMod
|
||||||
{
|
{
|
||||||
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
|
|
||||||
// make the map any harder and is more of a personal preference.
|
|
||||||
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
|
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
|
|
||||||
public override LocalisableString Description => "No more timing the end of hold notes.";
|
public override LocalisableString Description => "No more timing the end of hold notes.";
|
||||||
|
|
||||||
public override double ScoreMultiplier => 0.9;
|
|
||||||
|
|
||||||
public override IconUsage? Icon => OsuIcon.ModNoRelease;
|
public override IconUsage? Icon => OsuIcon.ModNoRelease;
|
||||||
|
|
||||||
public override ModType Type => ModType.DifficultyReduction;
|
public override ModType Type => ModType.DifficultyReduction;
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Game.Rulesets.Mania.Mods;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Scoring
|
||||||
|
{
|
||||||
|
public class ManiaScoreMultiplierCalculator : ScoreMultiplierCalculator
|
||||||
|
{
|
||||||
|
public ManiaScoreMultiplierCalculator(ScoreMultiplierContext context)
|
||||||
|
: base(context)
|
||||||
|
{
|
||||||
|
#region Difficulty Reduction
|
||||||
|
|
||||||
|
Single<ManiaModEasy>(hasMultiplier: 0.5);
|
||||||
|
Single<ManiaModNoFail>(hasMultiplier: 0.5);
|
||||||
|
Single<ManiaModHalfTime>(hasMultiplier: halfTime => rateAdjustMultiplier(halfTime.SpeedChange.Value));
|
||||||
|
Single<ManiaModDaycore>(hasMultiplier: daycore => rateAdjustMultiplier(daycore.SpeedChange.Value));
|
||||||
|
Single<ManiaModNoRelease>(hasMultiplier: 0.9);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Difficulty Increase
|
||||||
|
|
||||||
|
// Hard Rock
|
||||||
|
// Sudden Death
|
||||||
|
// Perfect
|
||||||
|
// Double Time
|
||||||
|
// Nightcore
|
||||||
|
// Fade In
|
||||||
|
// Hidden
|
||||||
|
// Cover
|
||||||
|
// Flashlight
|
||||||
|
// Accuracy Challenge
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Conversion
|
||||||
|
|
||||||
|
// Random
|
||||||
|
// Dual Stages
|
||||||
|
// Mirror
|
||||||
|
Single<ManiaModDifficultyAdjust>(hasMultiplier: 0.5);
|
||||||
|
Single<ManiaModClassic>(hasMultiplier: _ => classicMultiplier(Context.Score));
|
||||||
|
// Invert
|
||||||
|
Single<ManiaModConstantSpeed>(hasMultiplier: 0.9);
|
||||||
|
Single<ManiaModHoldOff>(hasMultiplier: 0.9);
|
||||||
|
Single<ManiaModKey1>(hasMultiplier: keyModMultiplier(Context.Score));
|
||||||
|
Single<ManiaModKey2>(hasMultiplier: keyModMultiplier(Context.Score));
|
||||||
|
Single<ManiaModKey3>(hasMultiplier: keyModMultiplier(Context.Score));
|
||||||
|
Single<ManiaModKey4>(hasMultiplier: keyModMultiplier(Context.Score));
|
||||||
|
Single<ManiaModKey5>(hasMultiplier: keyModMultiplier(Context.Score));
|
||||||
|
Single<ManiaModKey6>(hasMultiplier: keyModMultiplier(Context.Score));
|
||||||
|
Single<ManiaModKey7>(hasMultiplier: keyModMultiplier(Context.Score));
|
||||||
|
Single<ManiaModKey8>(hasMultiplier: keyModMultiplier(Context.Score));
|
||||||
|
Single<ManiaModKey9>(hasMultiplier: keyModMultiplier(Context.Score));
|
||||||
|
Single<ManiaModKey10>(hasMultiplier: keyModMultiplier(Context.Score));
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Automation
|
||||||
|
|
||||||
|
// Autoplay
|
||||||
|
// Cinema
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Fun
|
||||||
|
|
||||||
|
Single<ModWindUp>(hasMultiplier: 0.5);
|
||||||
|
Single<ModWindDown>(hasMultiplier: 0.5);
|
||||||
|
// Muted
|
||||||
|
Single<ModAdaptiveSpeed>(hasMultiplier: 0.5);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region System
|
||||||
|
|
||||||
|
// Score V2
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double rateAdjustMultiplier(double speedChange)
|
||||||
|
{
|
||||||
|
// Round to the nearest multiple of 0.1.
|
||||||
|
double value = (int)(speedChange * 10) / 10.0;
|
||||||
|
|
||||||
|
// Offset back to 0.
|
||||||
|
value -= 1;
|
||||||
|
|
||||||
|
if (speedChange >= 1)
|
||||||
|
return 1 + value / 5;
|
||||||
|
else
|
||||||
|
return 0.6 + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private const double old_key_mod_multiplier = 1;
|
||||||
|
private const double new_key_mod_multiplier = 0.9;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <para>
|
||||||
|
/// The mod multiplier was changed from 1.0x to 0.9x in https://github.com/ppy/osu/pull/30506
|
||||||
|
/// which was included in the https://osu.ppy.sh/home/changelog/tachyon/2025.718.0 release.
|
||||||
|
/// The replay version was not bumped in the change, meaning that the only usable indicator
|
||||||
|
/// of the mod multiplier changing is the client version.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Unfortunately not even the client version is available on server-side recorded replays
|
||||||
|
/// recorded prior to https://github.com/ppy/osu-server-spectator/pull/290,
|
||||||
|
/// which does not appear to have been deployed until August 1
|
||||||
|
/// (https://github.com/ppy/osu-server-spectator/releases/tag/2025.801.0).
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
private double keyModMultiplier(ScoreInfo? scoreInfo)
|
||||||
|
{
|
||||||
|
if (scoreInfo == null)
|
||||||
|
return new_key_mod_multiplier;
|
||||||
|
|
||||||
|
string clientVersion = scoreInfo.ClientVersion;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(clientVersion))
|
||||||
|
{
|
||||||
|
string[] pieces = clientVersion.Split('.');
|
||||||
|
|
||||||
|
if (int.TryParse(pieces[0], out int year) && int.TryParse(pieces[1], out int monthDay))
|
||||||
|
{
|
||||||
|
if (year < 2025 || (year == 2025 && monthDay < 718))
|
||||||
|
return old_key_mod_multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new_key_mod_multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client version not available, fallback to doing the best we can with the score's timestamp.
|
||||||
|
if (scoreInfo.Date < new DateTimeOffset(2025, 7, 18, 0, 0, 0, TimeSpan.Zero))
|
||||||
|
return old_key_mod_multiplier;
|
||||||
|
|
||||||
|
return new_key_mod_multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double classicMultiplier(ScoreInfo? score)
|
||||||
|
{
|
||||||
|
if (score != null && score.TotalScoreVersion < 30000017)
|
||||||
|
return 0.96;
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestTouchInputPlaceHitCircleDirectly()
|
public void TestTouchInputPlaceHitCircleDirectly()
|
||||||
{
|
{
|
||||||
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "HitCircle")));
|
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Hit circle")));
|
||||||
|
|
||||||
AddStep("tap to place circle", () => tap(this.ChildrenOfType<Playfield>().Single()));
|
AddStep("tap to place circle", () => tap(this.ChildrenOfType<Playfield>().Single()));
|
||||||
AddAssert("circle placed correctly", () =>
|
AddAssert("circle placed correctly", () =>
|
||||||
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestTouchInputPlaceCircleAfterTouchingComposeArea()
|
public void TestTouchInputPlaceCircleAfterTouchingComposeArea()
|
||||||
{
|
{
|
||||||
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "HitCircle")));
|
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Hit circle")));
|
||||||
|
|
||||||
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
|
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
|
||||||
AddAssert("circle placed", () => EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate) is HitCircle);
|
AddAssert("circle placed", () => EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate) is HitCircle);
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ using osu.Framework.Input;
|
|||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Osu.Edit;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
@@ -130,5 +133,74 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
AddAssert("slider has correct velocity", () => slider!.Velocity, () => Is.EqualTo(velocityBefore));
|
AddAssert("slider has correct velocity", () => slider!.Velocity, () => Is.EqualTo(velocityBefore));
|
||||||
AddAssert("slider has correct duration", () => slider!.Duration, () => Is.EqualTo(durationBefore));
|
AddAssert("slider has correct duration", () => slider!.Duration, () => Is.EqualTo(durationBefore));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestVelocityToolbox()
|
||||||
|
{
|
||||||
|
ExpandableSlider<double> velocitySlider = null!;
|
||||||
|
ExpandableButton useLastSliderButton = null!;
|
||||||
|
|
||||||
|
AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader()));
|
||||||
|
AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true);
|
||||||
|
AddStep("retrieve controls", () =>
|
||||||
|
{
|
||||||
|
var toolbox = this.ChildrenOfType<OsuSliderVelocityToolboxGroup>().Single();
|
||||||
|
velocitySlider = toolbox.ChildrenOfType<ExpandableSlider<double>>().Single();
|
||||||
|
useLastSliderButton = toolbox.ChildrenOfType<ExpandableButton>().Single();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("velocity slider at 1x", () => velocitySlider.Current.Value, () => Is.EqualTo(1));
|
||||||
|
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
|
||||||
|
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
|
||||||
|
AddAssert("use last slider button disabled", () => useLastSliderButton.Enabled.Value, () => Is.False);
|
||||||
|
|
||||||
|
AddStep("seek to 5000", () => editorClock.Seek(5000));
|
||||||
|
AddStep("set 2x velocity", () => velocitySlider.Current.Value = 2);
|
||||||
|
placeSlider();
|
||||||
|
AddAssert("placed slider has 2x velocity", () => editorBeatmap.HitObjects.OfType<Slider>().Last().SliderVelocityMultiplier, () => Is.EqualTo(2));
|
||||||
|
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
|
||||||
|
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
|
||||||
|
AddAssert("use last slider button enabled", () => useLastSliderButton.Enabled.Value, () => Is.True);
|
||||||
|
|
||||||
|
AddStep("seek to 6000", () => editorClock.Seek(6000));
|
||||||
|
placeSlider();
|
||||||
|
AddAssert("placed slider has 2x velocity", () => editorBeatmap.HitObjects.OfType<Slider>().Last().SliderVelocityMultiplier, () => Is.EqualTo(2));
|
||||||
|
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
|
||||||
|
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
|
||||||
|
AddAssert("use last slider button enabled", () => useLastSliderButton.Enabled.Value, () => Is.True);
|
||||||
|
|
||||||
|
AddStep("seek to 9000", () => editorClock.Seek(9000));
|
||||||
|
AddStep("set 3x velocity", () => velocitySlider.Current.Value = 3);
|
||||||
|
placeSlider();
|
||||||
|
AddAssert("placed slider has 3x velocity", () => editorBeatmap.HitObjects.OfType<Slider>().Last().SliderVelocityMultiplier, () => Is.EqualTo(3));
|
||||||
|
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
|
||||||
|
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
|
||||||
|
AddAssert("use last slider button enabled", () => useLastSliderButton.Enabled.Value, () => Is.True);
|
||||||
|
|
||||||
|
AddStep("seek to 10000", () => editorClock.Seek(10000));
|
||||||
|
AddStep("set 1x velocity", () => velocitySlider.Current.Value = 1);
|
||||||
|
AddStep("use last slider velocity instead", () => useLastSliderButton.TriggerClick());
|
||||||
|
placeSlider();
|
||||||
|
AddAssert("placed slider has 3x velocity", () => editorBeatmap.HitObjects.OfType<Slider>().Last().SliderVelocityMultiplier, () => Is.EqualTo(3));
|
||||||
|
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
|
||||||
|
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
|
||||||
|
AddAssert("use last slider button disabled", () => useLastSliderButton.Enabled.Value, () => Is.False);
|
||||||
|
|
||||||
|
AddStep("seek back to 7000", () => editorClock.Seek(7000));
|
||||||
|
placeSlider();
|
||||||
|
AddAssert("placed slider has 2x velocity", () => editorBeatmap.HitObjects.OfType<Slider>().ElementAt(2).SliderVelocityMultiplier, () => Is.EqualTo(2));
|
||||||
|
AddStep("expand right toolbox", () => InputManager.MoveMouseTo(this.ChildrenOfType<ExpandingToolboxContainer>().Last()));
|
||||||
|
AddUntilStep("wait for expand", () => useLastSliderButton.Expanded.Value, () => Is.True);
|
||||||
|
AddAssert("use last slider button disabled", () => useLastSliderButton.Enabled.Value, () => Is.False);
|
||||||
|
|
||||||
|
void placeSlider()
|
||||||
|
{
|
||||||
|
AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3));
|
||||||
|
AddStep("move mouse to top left", () => InputManager.MoveMouseTo(editor.ChildrenOfType<Playfield>().First().ScreenSpaceDrawQuad.TopLeft + new Vector2(50)));
|
||||||
|
AddStep("start placement", () => InputManager.Click(MouseButton.Left));
|
||||||
|
AddStep("move mouse to bottom right", () => InputManager.MoveMouseTo(editor.ChildrenOfType<Playfield>().First().ScreenSpaceDrawQuad.BottomRight - new Vector2(50)));
|
||||||
|
AddStep("end placement", () => InputManager.Click(MouseButton.Right));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
|||||||
{
|
{
|
||||||
public partial class TestSceneOsuModDifficultyAdjust : OsuModTestScene
|
public partial class TestSceneOsuModDifficultyAdjust : OsuModTestScene
|
||||||
{
|
{
|
||||||
|
protected override bool AllowFail => true;
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestNoAdjustment() => CreateModTest(new ModTestData
|
public void TestNoAdjustment() => CreateModTest(new ModTestData
|
||||||
{
|
{
|
||||||
@@ -72,6 +74,88 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
|||||||
PassCondition = () => checkSomeHit() && checkObjectsPreempt(450)
|
PassCondition = () => checkSomeHit() && checkObjectsPreempt(450)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScoreMultiplierCorrectWithNoAdjustment() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new OsuModDifficultyAdjust(),
|
||||||
|
CreateBeatmap = () => new Beatmap
|
||||||
|
{
|
||||||
|
BeatmapInfo = new BeatmapInfo
|
||||||
|
{
|
||||||
|
Difficulty = new BeatmapDifficulty
|
||||||
|
{
|
||||||
|
CircleSize = 8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 1000 },
|
||||||
|
new HitCircle { StartTime = 2000 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Autoplay = true,
|
||||||
|
PassCondition = () => Player.ScoreProcessor.TotalScore.Value == 1_000_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScoreMultiplierCorrectWithSingleAdjustment() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new OsuModDifficultyAdjust
|
||||||
|
{
|
||||||
|
ApproachRate = { Value = 7.3f }
|
||||||
|
},
|
||||||
|
CreateBeatmap = () => new Beatmap
|
||||||
|
{
|
||||||
|
BeatmapInfo = new BeatmapInfo
|
||||||
|
{
|
||||||
|
Difficulty = new BeatmapDifficulty
|
||||||
|
{
|
||||||
|
CircleSize = 8,
|
||||||
|
ApproachRate = 7,
|
||||||
|
OverallDifficulty = 6,
|
||||||
|
DrainRate = 5,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 1000 },
|
||||||
|
new HitCircle { StartTime = 2000 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Autoplay = true,
|
||||||
|
PassCondition = () => Player.ScoreProcessor.TotalScore.Value == 850_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScoreMultiplierCorrectWithMultipleAdjustments() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new OsuModDifficultyAdjust
|
||||||
|
{
|
||||||
|
ApproachRate = { Value = 6.8f },
|
||||||
|
OverallDifficulty = { Value = 6.6f }
|
||||||
|
},
|
||||||
|
CreateBeatmap = () => new Beatmap
|
||||||
|
{
|
||||||
|
BeatmapInfo = new BeatmapInfo
|
||||||
|
{
|
||||||
|
Difficulty = new BeatmapDifficulty
|
||||||
|
{
|
||||||
|
CircleSize = 8,
|
||||||
|
ApproachRate = 7,
|
||||||
|
OverallDifficulty = 6,
|
||||||
|
DrainRate = 5,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 1000 },
|
||||||
|
new HitCircle { StartTime = 2000 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Autoplay = true,
|
||||||
|
PassCondition = () => Player.ScoreProcessor.TotalScore.Value == 630_000,
|
||||||
|
});
|
||||||
|
|
||||||
private bool checkObjectsPreempt(double target)
|
private bool checkObjectsPreempt(double target)
|
||||||
{
|
{
|
||||||
var objects = Player.ChildrenOfType<DrawableHitCircle>();
|
var objects = Player.ChildrenOfType<DrawableHitCircle>();
|
||||||
|
|||||||
@@ -15,49 +15,25 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
|
||||||
|
|
||||||
[TestCase(6.5243170265483581d, 239, "diffcalc-test")]
|
[TestCase(6.6232533278125061d, 239, "diffcalc-test")]
|
||||||
[TestCase(1.3280410795791415d, 54, "zero-length-sliders")]
|
[TestCase(1.5045783545699611d, 54, "zero-length-sliders")]
|
||||||
[TestCase(0.40867325147697559d, 4, "very-fast-slider")]
|
[TestCase(0.43333836671191595d, 4, "very-fast-slider")]
|
||||||
[TestCase(0.87058175794353554d, 6, "nan-slider")]
|
[TestCase(0.13841532030395723d, 2, "nan-slider")]
|
||||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||||
|
|
||||||
[TestCase(9.4677607900646308d, 239, "diffcalc-test")]
|
[TestCase(9.6491691624112761d, 239, "diffcalc-test")]
|
||||||
[TestCase(1.6856612715618886d, 54, "zero-length-sliders")]
|
[TestCase(1.756936832498702d, 54, "zero-length-sliders")]
|
||||||
[TestCase(0.53588473186572561d, 4, "very-fast-slider")]
|
[TestCase(0.57771197086735004d, 4, "very-fast-slider")]
|
||||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
||||||
|
|
||||||
[TestCase(6.5243170265483581d, 239, "diffcalc-test")]
|
[TestCase(6.6232533278125061d, 239, "diffcalc-test")]
|
||||||
[TestCase(1.3280410795791415d, 54, "zero-length-sliders")]
|
[TestCase(1.5045783545699611d, 54, "zero-length-sliders")]
|
||||||
[TestCase(0.40867325147697559d, 4, "very-fast-slider")]
|
[TestCase(0.43333836671191595d, 4, "very-fast-slider")]
|
||||||
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
||||||
|
|
||||||
[TestCase(239, "diffcalc-test")]
|
|
||||||
[TestCase(54, "zero-length-sliders")]
|
|
||||||
[TestCase(4, "very-fast-slider")]
|
|
||||||
public void TestOffsetChanges(int expectedMaxCombo, string name)
|
|
||||||
{
|
|
||||||
const double offset_iterations = 400;
|
|
||||||
var beatmap = GetBeatmap(name);
|
|
||||||
|
|
||||||
var attributes = CreateDifficultyCalculator(beatmap).Calculate();
|
|
||||||
double expectedStarRating = attributes.StarRating;
|
|
||||||
|
|
||||||
for (int i = 0; i < offset_iterations; i++)
|
|
||||||
{
|
|
||||||
foreach (var beatmapHitObject in beatmap.Beatmap.HitObjects)
|
|
||||||
beatmapHitObject.StartTime++;
|
|
||||||
|
|
||||||
attributes = CreateDifficultyCalculator(beatmap).Calculate();
|
|
||||||
|
|
||||||
// Platform-dependent math functions (Pow, Cbrt, Exp, etc) may result in minute differences.
|
|
||||||
Assert.That(attributes.StarRating, Is.EqualTo(expectedStarRating).Within(0.00001));
|
|
||||||
Assert.That(attributes.MaxCombo, Is.EqualTo(expectedMaxCombo));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap);
|
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap);
|
||||||
|
|
||||||
protected override Ruleset CreateRuleset() => new OsuRuleset();
|
protected override Ruleset CreateRuleset() => new OsuRuleset();
|
||||||
|
|||||||
@@ -0,0 +1,297 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Tests.Rulesets;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
|
{
|
||||||
|
public class OsuScoreMultiplierTest : RulesetScoreMultiplierTest
|
||||||
|
{
|
||||||
|
public OsuScoreMultiplierTest()
|
||||||
|
: base(new OsuRuleset())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly object[][] test_cases =
|
||||||
|
[
|
||||||
|
#region Difficulty Reduction
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModEasy() }, 0.8],
|
||||||
|
[new Mod[] { new OsuModEasy { Retries = { Value = 1 } } }, 0.8],
|
||||||
|
[new Mod[] { new OsuModEasy { Retries = { Value = 3 } } }, 0.7],
|
||||||
|
[new Mod[] { new OsuModEasy { Retries = { Value = 5 } } }, 0.5],
|
||||||
|
[new Mod[] { new OsuModEasy { Retries = { Value = 8 } } }, 0.4],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModNoFail() }, 0.5],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.50 } } }, 0.20],
|
||||||
|
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.55 } } }, 0.27],
|
||||||
|
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.60 } } }, 0.34],
|
||||||
|
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.65 } } }, 0.41],
|
||||||
|
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.70 } } }, 0.48],
|
||||||
|
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.75 } } }, 0.55],
|
||||||
|
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.80 } } }, 0.62],
|
||||||
|
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.85 } } }, 0.69],
|
||||||
|
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.90 } } }, 0.76],
|
||||||
|
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.95 } } }, 0.83],
|
||||||
|
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.99 } } }, 0.83],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.50 } } }, 0.20],
|
||||||
|
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.55 } } }, 0.27],
|
||||||
|
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.60 } } }, 0.34],
|
||||||
|
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.65 } } }, 0.41],
|
||||||
|
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.70 } } }, 0.48],
|
||||||
|
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.75 } } }, 0.55],
|
||||||
|
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.80 } } }, 0.62],
|
||||||
|
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.85 } } }, 0.69],
|
||||||
|
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.90 } } }, 0.76],
|
||||||
|
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.95 } } }, 0.83],
|
||||||
|
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.99 } } }, 0.83],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Difficulty Increase
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModHardRock() }, 1.09],
|
||||||
|
[new Mod[] { new OsuModSuddenDeath() }, 1],
|
||||||
|
[new Mod[] { new OsuModPerfect() }, 1],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } }, 1.000],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.05 } } }, 1.000],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.10 } } }, 1.036],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.15 } } }, 1.036],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.20 } } }, 1.082],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } }, 1.082],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.30 } } }, 1.128],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.35 } } }, 1.128],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.40 } } }, 1.174],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.45 } } }, 1.174],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.50 } } }, 1.230],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.55 } } }, 1.230],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.60 } } }, 1.266],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.65 } } }, 1.266],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.70 } } }, 1.312],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.75 } } }, 1.312],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.80 } } }, 1.358],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.85 } } }, 1.358],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.90 } } }, 1.404],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.95 } } }, 1.404],
|
||||||
|
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2.00 } } }, 1.450],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.01 } } }, 1.000],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.05 } } }, 1.000],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.10 } } }, 1.036],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.15 } } }, 1.036],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.20 } } }, 1.082],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.25 } } }, 1.082],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.30 } } }, 1.128],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.35 } } }, 1.128],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.40 } } }, 1.174],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.45 } } }, 1.174],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.50 } } }, 1.230],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.55 } } }, 1.230],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.60 } } }, 1.266],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.65 } } }, 1.266],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.70 } } }, 1.312],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.75 } } }, 1.312],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.80 } } }, 1.358],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.85 } } }, 1.358],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.90 } } }, 1.404],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.95 } } }, 1.404],
|
||||||
|
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 2.00 } } }, 1.450],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModHidden() }, 1.04],
|
||||||
|
[new Mod[] { new OsuModHidden { OnlyFadeApproachCircles = { Value = true } } }, 1.02],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModTraceable() }, 1.02],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModFlashlight() }, 1.2],
|
||||||
|
[new Mod[] { new OsuModFlashlight { SizeMultiplier = { Value = 0.5f } } }, 1.2],
|
||||||
|
[new Mod[] { new OsuModFlashlight { SizeMultiplier = { Value = 0.9f } } }, 1.2],
|
||||||
|
[new Mod[] { new OsuModFlashlight { SizeMultiplier = { Value = 1.1f } } }, 1.18],
|
||||||
|
[new Mod[] { new OsuModFlashlight { SizeMultiplier = { Value = 1.5f } } }, 1.1],
|
||||||
|
[new Mod[] { new OsuModFlashlight { SizeMultiplier = { Value = 1.9f } } }, 1.02],
|
||||||
|
[new Mod[] { new OsuModFlashlight { SizeMultiplier = { Value = 2f } } }, 1.02],
|
||||||
|
[new Mod[] { new OsuModFlashlight { ComboBasedSize = { Value = false } } }, 1.04],
|
||||||
|
[new Mod[] { new OsuModFlashlight { SizeMultiplier = { Value = 1.9f }, ComboBasedSize = { Value = false } } }, 1.004],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModBlinds() }, 1.24],
|
||||||
|
[new Mod[] { new OsuModStrictTracking() }, 1],
|
||||||
|
[new Mod[] { new OsuModAccuracyChallenge() }, 1],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Conversion
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModTargetPractice() }, 0.01],
|
||||||
|
[new Mod[] { new OsuModDifficultyAdjust() }, 1],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModClassic() }, 0.985],
|
||||||
|
[new Mod[] { new OsuModClassic { ClassicNoteLock = { Value = false } } }, 0.96],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModRandom() }, 0.7],
|
||||||
|
[new Mod[] { new OsuModMirror() }, 1],
|
||||||
|
[new Mod[] { new OsuModAlternate() }, 1],
|
||||||
|
[new Mod[] { new OsuModSingleTap() }, 1],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Automation
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModAutoplay() }, 1],
|
||||||
|
[new Mod[] { new OsuModCinema() }, 1],
|
||||||
|
[new Mod[] { new OsuModRelax() }, 0.1],
|
||||||
|
[new Mod[] { new OsuModAutopilot() }, 0.1],
|
||||||
|
[new Mod[] { new OsuModSpunOut() }, 0.95],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Fun
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModTransform() }, 1],
|
||||||
|
[new Mod[] { new OsuModWiggle() }, 1],
|
||||||
|
[new Mod[] { new OsuModSpinIn() }, 1],
|
||||||
|
[new Mod[] { new OsuModGrow() }, 1],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModDeflate() }, 1],
|
||||||
|
[new Mod[] { new OsuModDeflate { StartScale = { Value = 5 } } }, 0.94],
|
||||||
|
|
||||||
|
[new Mod[] { new ModWindUp() }, 0.8 * 1 + 0.2 * 1.230],
|
||||||
|
[new Mod[] { new ModWindUp { InitialRate = { Value = 0.7 }, FinalRate = { Value = 1.2 } } }, 0.8 * 0.48 + 0.2 * 1.082],
|
||||||
|
[new Mod[] { new ModWindUp { InitialRate = { Value = 0.7 }, FinalRate = { Value = 0.9 } } }, 0.8 * 0.48 + 0.2 * 0.76],
|
||||||
|
[new Mod[] { new ModWindUp { InitialRate = { Value = 1.1 }, FinalRate = { Value = 1.4 } } }, 0.8 * 1.036 + 0.2 * 1.174],
|
||||||
|
|
||||||
|
[new Mod[] { new ModWindDown() }, 0.8 * 0.55 + 0.2 * 1],
|
||||||
|
[new Mod[] { new ModWindDown { InitialRate = { Value = 1.2 }, FinalRate = { Value = 0.7 } } }, 0.8 * 0.48 + 0.2 * 1.082],
|
||||||
|
[new Mod[] { new ModWindDown { InitialRate = { Value = 0.9 }, FinalRate = { Value = 0.7 } } }, 0.8 * 0.48 + 0.2 * 0.76],
|
||||||
|
[new Mod[] { new ModWindDown { InitialRate = { Value = 1.4 }, FinalRate = { Value = 1.1 } } }, 0.8 * 1.036 + 0.2 * 1.174],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModBarrelRoll() }, 1],
|
||||||
|
[new Mod[] { new OsuModApproachDifferent() }, 0.7],
|
||||||
|
[new Mod[] { new OsuModMuted() }, 1],
|
||||||
|
[new Mod[] { new OsuModNoScope() }, 1],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModMagnetised() }, 0.4],
|
||||||
|
[new Mod[] { new OsuModMagnetised { AttractionStrength = { Value = 0.05f } } }, 0.67],
|
||||||
|
[new Mod[] { new OsuModMagnetised { AttractionStrength = { Value = 0.2f } } }, 0.58],
|
||||||
|
[new Mod[] { new OsuModMagnetised { AttractionStrength = { Value = 0.7f } } }, 0.28],
|
||||||
|
[new Mod[] { new OsuModMagnetised { AttractionStrength = { Value = 1 } } }, 0.1],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModRepel() }, 1],
|
||||||
|
[new Mod[] { new ModAdaptiveSpeed() }, 0.1],
|
||||||
|
[new Mod[] { new OsuModFreezeFrame() }, 1],
|
||||||
|
[new Mod[] { new OsuModBubbles() }, 1],
|
||||||
|
[new Mod[] { new OsuModSynesthesia() }, 0.99],
|
||||||
|
[new Mod[] { new OsuModDepth() }, 1],
|
||||||
|
[new Mod[] { new OsuModBloom() }, 1],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region System
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModTouchDevice() }, 1],
|
||||||
|
[new Mod[] { new ModScoreV2() }, 1],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Combinations
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModHidden(), new OsuModHardRock() }, 1.04 * 1.09],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModHidden(), new OsuModWiggle() }, 1.02],
|
||||||
|
[new Mod[] { new OsuModHidden(), new OsuModGrow() }, 1.02],
|
||||||
|
[new Mod[] { new OsuModHidden(), new OsuModDeflate() }, 1.02],
|
||||||
|
[new Mod[] { new OsuModHidden(), new OsuModDeflate { StartScale = { Value = 4 } } }, 1.02 * 0.96],
|
||||||
|
[new Mod[] { new OsuModHidden(), new OsuModRepel() }, 1.02],
|
||||||
|
[new Mod[] { new OsuModHidden { OnlyFadeApproachCircles = { Value = true } }, new OsuModRepel() }, 1],
|
||||||
|
[new Mod[] { new OsuModHidden(), new OsuModDepth() }, 1.02],
|
||||||
|
[new Mod[] { new OsuModHidden(), new OsuModDepth(), new OsuModHardRock() }, 1.02 * 1.09],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModHidden(), new OsuModBlinds() }, 1.24],
|
||||||
|
[new Mod[] { new OsuModHidden(), new OsuModBlinds(), new OsuModHardRock() }, 1.24 * 1.09],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModTraceable(), new OsuModBlinds() }, 1.24],
|
||||||
|
[new Mod[] { new OsuModTraceable(), new OsuModBlinds(), new OsuModHardRock() }, 1.24 * 1.09],
|
||||||
|
|
||||||
|
[new Mod[] { new OsuModFlashlight(), new OsuModFreezeFrame() }, 1.1],
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
];
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(test_cases))]
|
||||||
|
public void TestMultipliers(Mod[] mods, double expectedMultiplier)
|
||||||
|
=> TestModCombination(mods, expectedMultiplier);
|
||||||
|
|
||||||
|
[TestCase(null, null, null, null, 1)]
|
||||||
|
[TestCase(2.9f, null, null, null, 0.95)]
|
||||||
|
[TestCase(3.1f, null, null, null, 0.95)]
|
||||||
|
[TestCase(null, 3.9f, null, null, 0.95)]
|
||||||
|
[TestCase(null, 4.1f, null, null, 0.95)]
|
||||||
|
[TestCase(null, null, 4.9f, null, 0.95)]
|
||||||
|
[TestCase(null, null, 5.1f, null, 0.95)]
|
||||||
|
[TestCase(null, null, null, 5.9f, 0.95)]
|
||||||
|
[TestCase(null, null, null, 6.1f, 0.95)]
|
||||||
|
[TestCase(2.9f, 3.9f, null, null, 0.95 * 0.95)]
|
||||||
|
[TestCase(2.9f, 3.9f, 4.9f, null, 0.95 * 0.95 * 0.95)]
|
||||||
|
[TestCase(2.9f, 3.9f, 4.9f, 5.9f, 0.95 * 0.95 * 0.95 * 0.95)]
|
||||||
|
[TestCase(0.0f, null, null, null, 0.1)]
|
||||||
|
[TestCase(0.0f, 0.0f, 0.0f, 0.0f, 0.1)]
|
||||||
|
public void TestDifficultyAdjust(float? cs, float? ar, float? od, float? hp, double expectedMultiplier)
|
||||||
|
{
|
||||||
|
var difficulty = new BeatmapDifficulty
|
||||||
|
{
|
||||||
|
CircleSize = 3,
|
||||||
|
ApproachRate = 4,
|
||||||
|
OverallDifficulty = 5,
|
||||||
|
DrainRate = 6,
|
||||||
|
};
|
||||||
|
var mod = new OsuModDifficultyAdjust
|
||||||
|
{
|
||||||
|
CircleSize = { Value = cs },
|
||||||
|
ApproachRate = { Value = ar },
|
||||||
|
OverallDifficulty = { Value = od },
|
||||||
|
DrainRate = { Value = hp },
|
||||||
|
};
|
||||||
|
|
||||||
|
var calculator = Ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(difficulty));
|
||||||
|
Assert.That(calculator.CalculateFor([mod]), Is.EqualTo(expectedMultiplier).Within(Precision.FLOAT_EPSILON));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(30000001, 0.96)]
|
||||||
|
[TestCase(30000009, 0.96)]
|
||||||
|
[TestCase(30000016, 0.96)]
|
||||||
|
[TestCase(30000017, 0.985)]
|
||||||
|
[TestCase(null, 0.985)]
|
||||||
|
public void TestClassicMultiplierVersioning(int? totalScoreVersion, double expectedMultiplier)
|
||||||
|
{
|
||||||
|
var scoreInfo = totalScoreVersion != null ? new ScoreInfo { TotalScoreVersion = totalScoreVersion.Value } : null;
|
||||||
|
var calculator = Ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty(), scoreInfo));
|
||||||
|
Assert.That(calculator.CalculateFor([new OsuModClassic()]), Is.EqualTo(expectedMultiplier).Within(Precision.DOUBLE_EPSILON));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void VerySmallModMultiplier()
|
||||||
|
{
|
||||||
|
var mods = new Mod[]
|
||||||
|
{
|
||||||
|
new OsuModEasy { Retries = { Value = 10 } },
|
||||||
|
new OsuModNoFail(),
|
||||||
|
new OsuModHalfTime { SpeedChange = { Value = 0.5 } },
|
||||||
|
new OsuModTargetPractice(),
|
||||||
|
new OsuModClassic { ClassicNoteLock = { Value = false } },
|
||||||
|
new OsuModDeflate { StartScale = { Value = 25 } },
|
||||||
|
new OsuModMagnetised { AttractionStrength = { Value = 1 } },
|
||||||
|
new OsuModSynesthesia(),
|
||||||
|
};
|
||||||
|
var calculator = Ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty()));
|
||||||
|
Assert.That(calculator.CalculateFor(mods), Is.GreaterThan(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"Mappings":[{"StartTime":76911.0,"Objects":[{"StartTime":76911.0,"EndTime":76911.0,"X":283.402,"Y":275.402}]},{"StartTime":77053.0,"Objects":[{"StartTime":77053.0,"EndTime":77053.0,"X":287.0515,"Y":279.0515}]},{"StartTime":77196.0,"Objects":[{"StartTime":77196.0,"EndTime":77196.0,"X":290.701019,"Y":282.701019}]},{"StartTime":77339.0,"Objects":[{"StartTime":77339.0,"EndTime":77339.0,"X":294.3505,"Y":286.3505}]},{"StartTime":77497.0,"Objects":[{"StartTime":77497.0,"EndTime":77497.0,"X":298.0,"Y":290.0,"StackOffset":{"X":0.0,"Y":0.0}},{"StartTime":77533.0,"EndTime":77533.0,"X":276.162567,"Y":293.0336,"StackOffset":{"X":0.0,"Y":0.0}}]}]}
|
{"Mappings":[{"StartTime":77497.0,"Objects":[{"StartTime":77497.0,"EndTime":77497.0,"X":298.0,"Y":290.0},{"StartTime":77533.0,"EndTime":77533.0,"X":276.162567,"Y":293.0336}]}]}
|
||||||
@@ -9,15 +9,10 @@ SliderMultiplier:2
|
|||||||
SliderTickRate:1
|
SliderTickRate:1
|
||||||
|
|
||||||
[TimingPoints]
|
[TimingPoints]
|
||||||
76911,285.7142857142857,4,1,0,100,1,8
|
|
||||||
77211,-100,4,3,50,70,0,0
|
77211,-100,4,3,50,70,0,0
|
||||||
77497,8.40402703648439,4,3,51,70,1,8
|
77497,8.40402703648439,4,3,51,70,1,8
|
||||||
77497,NaN,4,3,51,70,0,8
|
77497,NaN,4,3,51,70,0,8
|
||||||
77498,285.714285714286,4,3,51,70,1,0
|
77498,285.714285714286,4,3,51,70,1,0
|
||||||
|
|
||||||
[HitObjects]
|
[HitObjects]
|
||||||
298,290,76911,5,0,1:0:0:0:
|
|
||||||
298,290,77053,1,0,1:0:0:0:
|
|
||||||
298,290,77196,1,0,1:0:0:0:
|
|
||||||
298,290,77339,1,0,1:0:0:0:
|
|
||||||
298,290,77497,6,0,B|234:298|192:279|192:279|180:299|180:299|205:311|238:318|238:318|230:347|217:371|217:371|137:370|80:340|80:340|65:259|73:143|102:68|102:68|149:49|199:34|199:34|213:54|213:54|267:38|324:40|324:40|332:18|332:18|385:20|435:27|435:27|480:93|517:204|521:286|521:286|474:329|396:350|396:350|377:329|363:302|363:302|393:287|415:271|415:271|398:254|398:254|362:282|299:290,1,1723.66345596313,10|0,1:0|3:0,3:0:0:0:
|
298,290,77497,6,0,B|234:298|192:279|192:279|180:299|180:299|205:311|238:318|238:318|230:347|217:371|217:371|137:370|80:340|80:340|65:259|73:143|102:68|102:68|149:49|199:34|199:34|213:54|213:54|267:38|324:40|324:40|332:18|332:18|385:20|435:27|435:27|480:93|517:204|521:286|521:286|474:329|396:350|396:350|377:329|363:302|363:302|393:287|415:271|415:271|398:254|398:254|362:282|299:290,1,1723.66345596313,10|0,1:0|3:0,3:0:0:0:
|
||||||
|
|||||||
@@ -792,7 +792,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
AddStep("export beatmap", () =>
|
AddStep("export beatmap", () =>
|
||||||
{
|
{
|
||||||
var beatmapEncoder = new LegacyBeatmapEncoder(playableBeatmap, null);
|
var beatmapEncoder = new LegacyBeatmapEncoder(playableBeatmap, null, null);
|
||||||
|
|
||||||
using (var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osu"), FileMode.Create))
|
using (var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osu"), FileMode.Create))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,42 +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 osu.Game.Rulesets.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Aim
|
|
||||||
{
|
|
||||||
public static class AgilityEvaluator
|
|
||||||
{
|
|
||||||
private const double distance_cap = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.2; // 1.2 circles distance between centers
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Evaluates the difficulty of fast aiming
|
|
||||||
/// </summary>
|
|
||||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
if (current.BaseObject is Spinner)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
|
||||||
var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null;
|
|
||||||
|
|
||||||
double travelDistance = osuPrevObj?.LazyTravelDistance ?? 0;
|
|
||||||
double distance = travelDistance + osuCurrObj.LazyJumpDistance;
|
|
||||||
|
|
||||||
double distanceScaled = Math.Min(distance, distance_cap) / distance_cap;
|
|
||||||
|
|
||||||
double agilityDifficulty = distanceScaled * 1000 / osuCurrObj.AdjustedDeltaTime;
|
|
||||||
|
|
||||||
agilityDifficulty *= Math.Pow(osuCurrObj.SmallCircleBonus, 1.5);
|
|
||||||
|
|
||||||
agilityDifficulty *= highBpmBonus(osuCurrObj.AdjustedDeltaTime);
|
|
||||||
|
|
||||||
return agilityDifficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double highBpmBonus(double ms) => 1 / (1 - Math.Pow(0.2, ms / 1000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +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 osu.Game.Rulesets.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Aim
|
|
||||||
{
|
|
||||||
public static class FlowAimEvaluator
|
|
||||||
{
|
|
||||||
private const double velocity_change_multiplier = 0.52;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Evaluates difficulty of "flow aim" - aiming pattern where player doesn't stop their cursor on every object and instead "flows" through them.
|
|
||||||
/// </summary>
|
|
||||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliderTravelDistance)
|
|
||||||
{
|
|
||||||
if (current.BaseObject is Spinner || current.Index <= 1 || current.Previous(0).BaseObject is Spinner)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
|
||||||
var osuLastObj = (OsuDifficultyHitObject)current.Previous(0);
|
|
||||||
var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1);
|
|
||||||
|
|
||||||
double currDistance = withSliderTravelDistance ? osuCurrObj.LazyJumpDistance : osuCurrObj.JumpDistance;
|
|
||||||
double prevDistance = withSliderTravelDistance ? osuLastObj.LazyJumpDistance : osuLastObj.JumpDistance;
|
|
||||||
|
|
||||||
double currVelocity = currDistance / osuCurrObj.AdjustedDeltaTime;
|
|
||||||
|
|
||||||
if (osuLastObj.BaseObject is Slider && withSliderTravelDistance)
|
|
||||||
{
|
|
||||||
// If the last object is a slider, then we extend the travel velocity through the slider into the current object.
|
|
||||||
double sliderDistance = osuLastObj.LazyTravelDistance + osuCurrObj.LazyJumpDistance;
|
|
||||||
currVelocity = Math.Max(currVelocity, sliderDistance / osuCurrObj.AdjustedDeltaTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
double prevVelocity = prevDistance / osuLastObj.AdjustedDeltaTime;
|
|
||||||
|
|
||||||
double flowDifficulty = currVelocity;
|
|
||||||
|
|
||||||
// Apply high circle size bonus to the base velocity.
|
|
||||||
// We use reduced CS bonus here because the bonus was made for an evaluator with a different d/t scaling
|
|
||||||
flowDifficulty *= Math.Sqrt(osuCurrObj.SmallCircleBonus);
|
|
||||||
|
|
||||||
// Rhythm changes are harder to flow
|
|
||||||
flowDifficulty *= 1 + Math.Min(0.25,
|
|
||||||
Math.Pow((Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) - Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime)) / 50, 4));
|
|
||||||
|
|
||||||
if (osuCurrObj.Angle != null && osuLastObj.Angle != null)
|
|
||||||
{
|
|
||||||
double angleDifference = Math.Abs(osuCurrObj.Angle.Value - osuLastObj.Angle.Value);
|
|
||||||
double angleDifferenceAdjusted = Math.Sin(angleDifference / 2) * 180.0;
|
|
||||||
double angularVelocity = angleDifferenceAdjusted / (osuCurrObj.AdjustedDeltaTime * 0.1);
|
|
||||||
|
|
||||||
// Low angular velocity flow (angles are consistent) is easier to follow than erratic flow
|
|
||||||
flowDifficulty *= 0.8 + Math.Sqrt(angularVelocity / 270.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If all three notes are overlapping - don't reward bonuses as you don't have to do additional movement
|
|
||||||
double overlappedNotesWeight = 1;
|
|
||||||
|
|
||||||
if (current.Index > 2)
|
|
||||||
{
|
|
||||||
double o1 = calculateOverlapFactor(osuCurrObj, osuLastObj);
|
|
||||||
double o2 = calculateOverlapFactor(osuCurrObj, osuLastLastObj);
|
|
||||||
double o3 = calculateOverlapFactor(osuLastObj, osuLastLastObj);
|
|
||||||
|
|
||||||
overlappedNotesWeight = 1 - o1 * o2 * o3;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (osuCurrObj.Angle != null)
|
|
||||||
{
|
|
||||||
// Acute angles are also hard to flow
|
|
||||||
flowDifficulty += currVelocity *
|
|
||||||
SnapAimEvaluator.CalcAngleAcuteness(osuCurrObj.Angle.Value) *
|
|
||||||
overlappedNotesWeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.Max(prevVelocity, currVelocity) != 0)
|
|
||||||
{
|
|
||||||
if (withSliderTravelDistance)
|
|
||||||
{
|
|
||||||
currVelocity = currDistance / osuCurrObj.AdjustedDeltaTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scale with ratio of difference compared to 0.5 * max dist.
|
|
||||||
double distRatio = DifficultyCalculationUtils.Smoothstep(Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity), 0, 1);
|
|
||||||
|
|
||||||
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
|
||||||
double overlapVelocityBuff = Math.Min(OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25 / Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime),
|
|
||||||
Math.Abs(prevVelocity - currVelocity));
|
|
||||||
|
|
||||||
flowDifficulty += overlapVelocityBuff *
|
|
||||||
distRatio *
|
|
||||||
overlappedNotesWeight *
|
|
||||||
velocity_change_multiplier;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (osuCurrObj.BaseObject is Slider && withSliderTravelDistance)
|
|
||||||
{
|
|
||||||
// Include slider velocity to make velocity more consistent with snap
|
|
||||||
flowDifficulty += osuCurrObj.TravelDistance / osuCurrObj.TravelTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final velocity is being raised to a power because flow difficulty scales harder with both high distance and time, and we want to account for that
|
|
||||||
flowDifficulty = Math.Pow(flowDifficulty, 1.45);
|
|
||||||
|
|
||||||
// Reduce difficulty for low spacing since spacing below radius is always to be flowed
|
|
||||||
return flowDifficulty * DifficultyCalculationUtils.Smootherstep(currDistance, 0, OsuDifficultyHitObject.NORMALISED_RADIUS);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double calculateOverlapFactor(OsuDifficultyHitObject first, OsuDifficultyHitObject second)
|
|
||||||
{
|
|
||||||
var firstBase = (OsuHitObject)first.BaseObject;
|
|
||||||
var secondBase = (OsuHitObject)second.BaseObject;
|
|
||||||
double objectRadius = firstBase.Radius;
|
|
||||||
|
|
||||||
double distance = Vector2.Distance(firstBase.StackedPosition, secondBase.StackedPosition);
|
|
||||||
return Math.Clamp(1 - Math.Pow(Math.Max(distance - objectRadius, 0) / objectRadius, 2), 0, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,220 +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 osu.Framework.Extensions.ObjectExtensions;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Aim
|
|
||||||
{
|
|
||||||
public static class SnapAimEvaluator
|
|
||||||
{
|
|
||||||
private const double wide_angle_multiplier = 9.67;
|
|
||||||
private const double acute_angle_multiplier = 2.41;
|
|
||||||
private const double slider_multiplier = 1.5;
|
|
||||||
private const double velocity_change_multiplier = 0.9;
|
|
||||||
private const double wiggle_multiplier = 1.02; // WARNING: Increasing this multiplier beyond 1.02 reduces difficulty as distance increases. Refer to the desmos link above the wiggle bonus calculation
|
|
||||||
private const double maximum_repetition_nerf = 0.15;
|
|
||||||
private const double maximum_vector_influence = 0.5;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Evaluates the difficulty of aiming the current object, based on:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item><description>cursor velocity to the current object,</description></item>
|
|
||||||
/// <item><description>angle difficulty,</description></item>
|
|
||||||
/// <item><description>sharp velocity increases,</description></item>
|
|
||||||
/// <item><description>and slider difficulty.</description></item>
|
|
||||||
/// </list>
|
|
||||||
/// </summary>
|
|
||||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliderTravelDistance)
|
|
||||||
{
|
|
||||||
if (current.BaseObject is Spinner || current.Index <= 1 || current.Previous(0).BaseObject is Spinner)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
|
||||||
var osuLastObj = (OsuDifficultyHitObject)current.Previous(0);
|
|
||||||
var osuLast2Obj = (OsuDifficultyHitObject)current.Previous(2);
|
|
||||||
|
|
||||||
const int radius = OsuDifficultyHitObject.NORMALISED_RADIUS;
|
|
||||||
const int diameter = OsuDifficultyHitObject.NORMALISED_DIAMETER;
|
|
||||||
|
|
||||||
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
|
|
||||||
double currDistance = withSliderTravelDistance ? osuCurrObj.LazyJumpDistance : osuCurrObj.JumpDistance;
|
|
||||||
double currVelocity = currDistance / osuCurrObj.AdjustedDeltaTime;
|
|
||||||
|
|
||||||
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
|
|
||||||
if (osuLastObj.BaseObject is Slider && withSliderTravelDistance)
|
|
||||||
{
|
|
||||||
double sliderDistance = osuLastObj.LazyTravelDistance + osuCurrObj.LazyJumpDistance;
|
|
||||||
currVelocity = Math.Max(currVelocity, sliderDistance / osuCurrObj.AdjustedDeltaTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
double prevDistance = withSliderTravelDistance ? osuLastObj.LazyJumpDistance : osuLastObj.JumpDistance;
|
|
||||||
double prevVelocity = prevDistance / osuLastObj.AdjustedDeltaTime;
|
|
||||||
|
|
||||||
double snapDifficulty = currVelocity; // Start difficulty with regular velocity.
|
|
||||||
|
|
||||||
// Penalize angle repetition.
|
|
||||||
snapDifficulty *= vectorAngleRepetition(osuCurrObj, osuLastObj);
|
|
||||||
|
|
||||||
if (osuCurrObj.Angle != null && osuLastObj.Angle != null)
|
|
||||||
{
|
|
||||||
double currAngle = osuCurrObj.Angle.Value;
|
|
||||||
double lastAngle = osuLastObj.Angle.Value;
|
|
||||||
|
|
||||||
// Rewarding angles, take the smaller velocity as base.
|
|
||||||
double velocityInfluence = Math.Min(currVelocity, prevVelocity);
|
|
||||||
|
|
||||||
double acuteAngleBonus = 0;
|
|
||||||
|
|
||||||
if (Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) < 1.25 * Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime)) // If rhythms are the same.
|
|
||||||
{
|
|
||||||
acuteAngleBonus = CalcAngleAcuteness(currAngle);
|
|
||||||
|
|
||||||
// Penalize angle repetition. It is important to do it _before_ multiplying by anything because we compare raw acuteness here
|
|
||||||
acuteAngleBonus *= 0.08 + 0.92 * (1 - Math.Min(acuteAngleBonus, Math.Pow(CalcAngleAcuteness(lastAngle), 3)));
|
|
||||||
|
|
||||||
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
|
||||||
acuteAngleBonus *= velocityInfluence * DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.AdjustedDeltaTime, 2), 300, 400) *
|
|
||||||
DifficultyCalculationUtils.Smootherstep(currDistance, 0, diameter * 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
double wideAngleBonus = calcAngleWideness(currAngle);
|
|
||||||
|
|
||||||
// Penalize angle repetition. It is important to do it _before_ multiplying by velocity because we compare raw wideness here
|
|
||||||
wideAngleBonus *= 0.25 + 0.75 * (1 - Math.Min(wideAngleBonus, Math.Pow(calcAngleWideness(lastAngle), 3)));
|
|
||||||
|
|
||||||
// Rescaling velocity for the wide angle bonus
|
|
||||||
const double wide_angle_time_scale = 1.45;
|
|
||||||
double wideAngleCurrVelocity = currDistance / Math.Pow(osuCurrObj.AdjustedDeltaTime, wide_angle_time_scale);
|
|
||||||
double wideAnglePrevVelocity = prevDistance / Math.Pow(osuLastObj.AdjustedDeltaTime, wide_angle_time_scale);
|
|
||||||
|
|
||||||
if (osuLastObj.BaseObject is Slider && withSliderTravelDistance)
|
|
||||||
{
|
|
||||||
double sliderDistance = osuLastObj.LazyTravelDistance + osuCurrObj.LazyJumpDistance;
|
|
||||||
wideAngleCurrVelocity = Math.Max(wideAngleCurrVelocity, sliderDistance / Math.Pow(osuCurrObj.AdjustedDeltaTime, wide_angle_time_scale));
|
|
||||||
}
|
|
||||||
|
|
||||||
wideAngleBonus *= Math.Min(wideAngleCurrVelocity, wideAnglePrevVelocity);
|
|
||||||
|
|
||||||
if (osuLast2Obj != null)
|
|
||||||
{
|
|
||||||
// If objects just go back and forth through a middle point - don't give as much wide bonus
|
|
||||||
// Use Previous(2) and Previous(0) because angles calculation is done prevprev-prev-curr, so any object's angle's center point is always the previous object
|
|
||||||
var lastBaseObject = (OsuHitObject)osuLastObj.BaseObject;
|
|
||||||
var last2BaseObject = (OsuHitObject)osuLast2Obj.BaseObject;
|
|
||||||
|
|
||||||
float distance = (last2BaseObject.StackedPosition - lastBaseObject.StackedPosition).Length;
|
|
||||||
|
|
||||||
if (distance < 1)
|
|
||||||
{
|
|
||||||
wideAngleBonus *= 1 - 0.55 * (1 - distance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add in acute angle bonus or wide angle bonus, whichever is larger.
|
|
||||||
snapDifficulty += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier);
|
|
||||||
|
|
||||||
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
|
||||||
// https://www.desmos.com/calculator/dp0v0nvowc
|
|
||||||
double wiggleBonus = velocityInfluence
|
|
||||||
* DifficultyCalculationUtils.Smootherstep(currDistance, radius, diameter)
|
|
||||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(currDistance, diameter * 3, diameter), 1.8)
|
|
||||||
* DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60))
|
|
||||||
* DifficultyCalculationUtils.Smootherstep(prevDistance, radius, diameter)
|
|
||||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(prevDistance, diameter * 3, diameter), 1.8)
|
|
||||||
* DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60));
|
|
||||||
|
|
||||||
snapDifficulty += wiggleBonus * wiggle_multiplier;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.Max(prevVelocity, currVelocity) != 0)
|
|
||||||
{
|
|
||||||
if (withSliderTravelDistance)
|
|
||||||
{
|
|
||||||
// We want to use just the object jump without slider velocity when awarding differences
|
|
||||||
currVelocity = currDistance / osuCurrObj.AdjustedDeltaTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scale with ratio of difference compared to 0.5 * max dist.
|
|
||||||
double distRatio = DifficultyCalculationUtils.Smoothstep(Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity), 0, 1);
|
|
||||||
|
|
||||||
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
|
||||||
double overlapVelocityBuff = Math.Min(diameter * 1.25 / Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), Math.Abs(prevVelocity - currVelocity));
|
|
||||||
|
|
||||||
double velocityChangeBonus = overlapVelocityBuff * distRatio;
|
|
||||||
|
|
||||||
// Penalize for rhythm changes.
|
|
||||||
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) / Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), 2);
|
|
||||||
|
|
||||||
snapDifficulty += velocityChangeBonus * velocity_change_multiplier;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reward sliders based on velocity.
|
|
||||||
if (osuCurrObj.BaseObject is Slider && withSliderTravelDistance)
|
|
||||||
{
|
|
||||||
double sliderBonus = osuCurrObj.TravelDistance / osuCurrObj.TravelTime;
|
|
||||||
snapDifficulty += (sliderBonus < 1 ? sliderBonus : Math.Pow(sliderBonus, 0.75)) * slider_multiplier;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply high circle size bonus
|
|
||||||
snapDifficulty *= osuCurrObj.SmallCircleBonus;
|
|
||||||
|
|
||||||
snapDifficulty *= highBpmBonus(osuCurrObj.AdjustedDeltaTime);
|
|
||||||
|
|
||||||
return snapDifficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double highBpmBonus(double ms) => 1 / (1 - Math.Pow(0.03, Math.Pow(ms / 1000, 0.65)));
|
|
||||||
|
|
||||||
private static double vectorAngleRepetition(OsuDifficultyHitObject current, OsuDifficultyHitObject previous)
|
|
||||||
{
|
|
||||||
if (current.Angle == null || previous.Angle == null)
|
|
||||||
return 1;
|
|
||||||
|
|
||||||
const double note_limit = 6;
|
|
||||||
|
|
||||||
double constantAngleCount = 0;
|
|
||||||
|
|
||||||
for (int index = 0; index < note_limit; index++)
|
|
||||||
{
|
|
||||||
var loopObj = (OsuDifficultyHitObject)current.Previous(index);
|
|
||||||
|
|
||||||
if (loopObj.IsNull())
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Only consider vectors in the same jump section, stopping to change rhythm ruins momentum
|
|
||||||
if (Math.Max(current.AdjustedDeltaTime, loopObj.AdjustedDeltaTime) > 1.1 * Math.Min(current.AdjustedDeltaTime, loopObj.AdjustedDeltaTime))
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (loopObj.NormalisedVectorAngle.IsNotNull() && current.NormalisedVectorAngle.IsNotNull())
|
|
||||||
{
|
|
||||||
double angleDifference = Math.Abs(current.NormalisedVectorAngle.Value - loopObj.NormalisedVectorAngle.Value);
|
|
||||||
// Refer to this desmos for tuning, constants need to be precise so that values stay within the range of 0 and 1.
|
|
||||||
// https://www.desmos.com/calculator/a8jesv5sv2
|
|
||||||
constantAngleCount += Math.Cos(8 * Math.Min(double.DegreesToRadians(11.25), angleDifference));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
double vectorRepetition = Math.Pow(Math.Min(0.5 / constantAngleCount, 1), 2);
|
|
||||||
|
|
||||||
double stackFactor = DifficultyCalculationUtils.Smootherstep(current.LazyJumpDistance, 0, OsuDifficultyHitObject.NORMALISED_DIAMETER);
|
|
||||||
|
|
||||||
double currAngle = current.Angle.Value;
|
|
||||||
double lastAngle = previous.Angle.Value;
|
|
||||||
|
|
||||||
double angleDifferenceAdjusted = Math.Cos(2 * Math.Min(double.DegreesToRadians(45), Math.Abs(currAngle - lastAngle) * stackFactor));
|
|
||||||
|
|
||||||
double baseNerf = 1 - maximum_repetition_nerf * CalcAngleAcuteness(lastAngle) * angleDifferenceAdjusted;
|
|
||||||
|
|
||||||
return Math.Pow(baseNerf + (1 - baseNerf) * vectorRepetition * maximum_vector_influence * stackFactor, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double calcAngleWideness(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(40), double.DegreesToRadians(140));
|
|
||||||
|
|
||||||
public static double CalcAngleAcuteness(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(140), double.DegreesToRadians(40));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Utils;
|
||||||
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||||
|
{
|
||||||
|
public static class AimEvaluator
|
||||||
|
{
|
||||||
|
private const double wide_angle_multiplier = 1.5;
|
||||||
|
private const double acute_angle_multiplier = 2.55;
|
||||||
|
private const double slider_multiplier = 1.35;
|
||||||
|
private const double velocity_change_multiplier = 0.75;
|
||||||
|
private const double wiggle_multiplier = 1.02;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Evaluates the difficulty of aiming the current object, based on:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><description>cursor velocity to the current object,</description></item>
|
||||||
|
/// <item><description>angle difficulty,</description></item>
|
||||||
|
/// <item><description>sharp velocity increases,</description></item>
|
||||||
|
/// <item><description>and slider difficulty.</description></item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliderTravelDistance)
|
||||||
|
{
|
||||||
|
if (current.BaseObject is Spinner || current.Index <= 1 || current.Previous(0).BaseObject is Spinner)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||||
|
var osuLastObj = (OsuDifficultyHitObject)current.Previous(0);
|
||||||
|
var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1);
|
||||||
|
var osuLast2Obj = (OsuDifficultyHitObject)current.Previous(2);
|
||||||
|
|
||||||
|
const int radius = OsuDifficultyHitObject.NORMALISED_RADIUS;
|
||||||
|
const int diameter = OsuDifficultyHitObject.NORMALISED_DIAMETER;
|
||||||
|
|
||||||
|
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
|
||||||
|
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.AdjustedDeltaTime;
|
||||||
|
|
||||||
|
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
|
||||||
|
if (osuLastObj.BaseObject is Slider && withSliderTravelDistance)
|
||||||
|
{
|
||||||
|
double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; // calculate the slider velocity from slider head to slider end.
|
||||||
|
double movementVelocity = osuCurrObj.MinimumJumpDistance / osuCurrObj.MinimumJumpTime; // calculate the movement velocity from slider end to current object
|
||||||
|
|
||||||
|
currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity.
|
||||||
|
}
|
||||||
|
|
||||||
|
// As above, do the same for the previous hitobject.
|
||||||
|
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.AdjustedDeltaTime;
|
||||||
|
|
||||||
|
if (osuLastLastObj.BaseObject is Slider && withSliderTravelDistance)
|
||||||
|
{
|
||||||
|
double travelVelocity = osuLastLastObj.TravelDistance / osuLastLastObj.TravelTime;
|
||||||
|
double movementVelocity = osuLastObj.MinimumJumpDistance / osuLastObj.MinimumJumpTime;
|
||||||
|
|
||||||
|
prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
double wideAngleBonus = 0;
|
||||||
|
double acuteAngleBonus = 0;
|
||||||
|
double sliderBonus = 0;
|
||||||
|
double velocityChangeBonus = 0;
|
||||||
|
double wiggleBonus = 0;
|
||||||
|
|
||||||
|
double aimStrain = currVelocity; // Start strain with regular velocity.
|
||||||
|
|
||||||
|
if (osuCurrObj.Angle != null && osuLastObj.Angle != null)
|
||||||
|
{
|
||||||
|
double currAngle = osuCurrObj.Angle.Value;
|
||||||
|
double lastAngle = osuLastObj.Angle.Value;
|
||||||
|
|
||||||
|
// Rewarding angles, take the smaller velocity as base.
|
||||||
|
double angleBonus = Math.Min(currVelocity, prevVelocity);
|
||||||
|
|
||||||
|
if (Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) < 1.25 * Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime)) // If rhythms are the same.
|
||||||
|
{
|
||||||
|
acuteAngleBonus = calcAcuteAngleBonus(currAngle);
|
||||||
|
|
||||||
|
// Penalize angle repetition.
|
||||||
|
acuteAngleBonus *= 0.08 + 0.92 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
|
||||||
|
|
||||||
|
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
||||||
|
acuteAngleBonus *= angleBonus *
|
||||||
|
DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.AdjustedDeltaTime, 2), 300, 400) *
|
||||||
|
DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
wideAngleBonus = calcWideAngleBonus(currAngle);
|
||||||
|
|
||||||
|
// Penalize angle repetition.
|
||||||
|
wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3));
|
||||||
|
|
||||||
|
// Apply full wide angle bonus for distance more than one diameter
|
||||||
|
wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter);
|
||||||
|
|
||||||
|
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
||||||
|
// https://www.desmos.com/calculator/dp0v0nvowc
|
||||||
|
wiggleBonus = angleBonus
|
||||||
|
* DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter)
|
||||||
|
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
||||||
|
* DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60))
|
||||||
|
* DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter)
|
||||||
|
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
||||||
|
* DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60));
|
||||||
|
|
||||||
|
if (osuLast2Obj != null)
|
||||||
|
{
|
||||||
|
// If objects just go back and forth through a middle point - don't give as much wide bonus
|
||||||
|
// Use Previous(2) and Previous(0) because angles calculation is done prevprev-prev-curr, so any object's angle's center point is always the previous object
|
||||||
|
var lastBaseObject = (OsuHitObject)osuLastObj.BaseObject;
|
||||||
|
var last2BaseObject = (OsuHitObject)osuLast2Obj.BaseObject;
|
||||||
|
|
||||||
|
float distance = (last2BaseObject.StackedPosition - lastBaseObject.StackedPosition).Length;
|
||||||
|
|
||||||
|
if (distance < 1)
|
||||||
|
{
|
||||||
|
wideAngleBonus *= 1 - 0.35 * (1 - distance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.Max(prevVelocity, currVelocity) != 0)
|
||||||
|
{
|
||||||
|
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
|
||||||
|
prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.AdjustedDeltaTime;
|
||||||
|
currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.AdjustedDeltaTime;
|
||||||
|
|
||||||
|
// Scale with ratio of difference compared to 0.5 * max dist.
|
||||||
|
double distRatio = DifficultyCalculationUtils.Smoothstep(Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity), 0, 1);
|
||||||
|
|
||||||
|
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
||||||
|
double overlapVelocityBuff = Math.Min(diameter * 1.25 / Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), Math.Abs(prevVelocity - currVelocity));
|
||||||
|
|
||||||
|
velocityChangeBonus = overlapVelocityBuff * distRatio;
|
||||||
|
|
||||||
|
// Penalize for rhythm changes.
|
||||||
|
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) / Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (osuLastObj.BaseObject is Slider)
|
||||||
|
{
|
||||||
|
// Reward sliders based on velocity.
|
||||||
|
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
aimStrain += wiggleBonus * wiggle_multiplier;
|
||||||
|
aimStrain += velocityChangeBonus * velocity_change_multiplier;
|
||||||
|
|
||||||
|
// Add in acute angle bonus or wide angle bonus, whichever is larger.
|
||||||
|
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier);
|
||||||
|
|
||||||
|
// Apply high circle size bonus
|
||||||
|
aimStrain *= osuCurrObj.SmallCircleBonus;
|
||||||
|
|
||||||
|
// Add in additional slider velocity bonus.
|
||||||
|
if (withSliderTravelDistance)
|
||||||
|
aimStrain += sliderBonus * slider_multiplier;
|
||||||
|
|
||||||
|
return aimStrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(40), double.DegreesToRadians(140));
|
||||||
|
|
||||||
|
private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(140), double.DegreesToRadians(40));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,8 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||||
@@ -32,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
/// <item><description>and whether the hidden mod is enabled.</description></item>
|
/// <item><description>and whether the hidden mod is enabled.</description></item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, IReadOnlyList<Mod> mods)
|
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool hidden)
|
||||||
{
|
{
|
||||||
if (current.BaseObject is Spinner)
|
if (current.BaseObject is Spinner)
|
||||||
return 0;
|
return 0;
|
||||||
@@ -44,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
double smallDistNerf = 1.0;
|
double smallDistNerf = 1.0;
|
||||||
double cumulativeStrainTime = 0.0;
|
double cumulativeStrainTime = 0.0;
|
||||||
|
|
||||||
double flashlightDifficulty = 0.0;
|
double result = 0.0;
|
||||||
|
|
||||||
OsuDifficultyHitObject lastObj = osuCurrent;
|
OsuDifficultyHitObject lastObj = osuCurrent;
|
||||||
|
|
||||||
@@ -70,9 +66,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
double stackNerf = Math.Min(1.0, (currentObj.LazyJumpDistance / scalingFactor) / 25.0);
|
double stackNerf = Math.Min(1.0, (currentObj.LazyJumpDistance / scalingFactor) / 25.0);
|
||||||
|
|
||||||
// Bonus based on how visible the object is.
|
// Bonus based on how visible the object is.
|
||||||
double opacityBonus = 1.0 + max_opacity_bonus * (1.0 - osuCurrent.OpacityAt(currentHitObject.StartTime, mods.OfType<OsuModHidden>().Any(m => !m.OnlyFadeApproachCircles.Value)));
|
double opacityBonus = 1.0 + max_opacity_bonus * (1.0 - osuCurrent.OpacityAt(currentHitObject.StartTime, hidden));
|
||||||
|
|
||||||
flashlightDifficulty += stackNerf * opacityBonus * scalingFactor * jumpDistance / cumulativeStrainTime;
|
result += stackNerf * opacityBonus * scalingFactor * jumpDistance / cumulativeStrainTime;
|
||||||
|
|
||||||
if (currentObj.Angle != null && osuCurrent.Angle != null)
|
if (currentObj.Angle != null && osuCurrent.Angle != null)
|
||||||
{
|
{
|
||||||
@@ -85,14 +81,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
lastObj = currentObj;
|
lastObj = currentObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
flashlightDifficulty = Math.Pow(smallDistNerf * flashlightDifficulty, 2.0);
|
result = Math.Pow(smallDistNerf * result, 2.0);
|
||||||
|
|
||||||
// Additional bonus for Hidden due to there being no approach circles.
|
// Additional bonus for Hidden due to there being no approach circles.
|
||||||
if (mods.OfType<OsuModHidden>().Any())
|
if (hidden)
|
||||||
flashlightDifficulty *= 1.0 + hidden_bonus;
|
result *= 1.0 + hidden_bonus;
|
||||||
|
|
||||||
// Nerf patterns with repeated angles.
|
// Nerf patterns with repeated angles.
|
||||||
flashlightDifficulty *= min_angle_multiplier + (1.0 - min_angle_multiplier) / (angleRepeatCount + 1.0);
|
result *= min_angle_multiplier + (1.0 - min_angle_multiplier) / (angleRepeatCount + 1.0);
|
||||||
|
|
||||||
double sliderBonus = 0.0;
|
double sliderBonus = 0.0;
|
||||||
|
|
||||||
@@ -112,9 +108,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
sliderBonus /= (osuSlider.RepeatCount + 1);
|
sliderBonus /= (osuSlider.RepeatCount + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
flashlightDifficulty += sliderBonus * slider_multiplier;
|
result += sliderBonus * slider_multiplier;
|
||||||
|
|
||||||
return flashlightDifficulty;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,272 +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 osu.Framework.Extensions.ObjectExtensions;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|
||||||
{
|
|
||||||
public static class ReadingEvaluator
|
|
||||||
{
|
|
||||||
private const double reading_window_size = 3000; // 3 seconds
|
|
||||||
private const double distance_influence_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.5; // 1.5 circles distance between centers
|
|
||||||
private const double hidden_multiplier = 0.28;
|
|
||||||
private const double density_multiplier = 2.4;
|
|
||||||
private const double density_difficulty_base = 2.5;
|
|
||||||
private const double preempt_balancing_factor = 140000;
|
|
||||||
private const double preempt_starting_point = 500; // AR 9.66 in milliseconds
|
|
||||||
private const double minimum_angle_relevancy_time = 2000; // 2 seconds
|
|
||||||
private const double maximum_angle_relevancy_time = 200;
|
|
||||||
|
|
||||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool hidden)
|
|
||||||
{
|
|
||||||
if (current.BaseObject is Spinner || current.Index == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
var currObj = (OsuDifficultyHitObject)current;
|
|
||||||
var nextObj = (OsuDifficultyHitObject)current.Next(0);
|
|
||||||
|
|
||||||
double velocity = Math.Max(1, currObj.LazyJumpDistance / currObj.AdjustedDeltaTime); // Only allow velocity to buff
|
|
||||||
|
|
||||||
double currentVisibleObjectDensity = retrieveCurrentVisibleObjectDensity(currObj);
|
|
||||||
double pastObjectDifficultyInfluence = getPastObjectDifficultyInfluence(currObj);
|
|
||||||
|
|
||||||
double constantAngleNerfFactor = getConstantAngleNerfFactor(currObj);
|
|
||||||
|
|
||||||
double noteDensityDifficulty = calculateDensityDifficulty(nextObj, velocity, constantAngleNerfFactor, pastObjectDifficultyInfluence, currentVisibleObjectDensity);
|
|
||||||
|
|
||||||
double hiddenDifficulty = hidden
|
|
||||||
? calculateHiddenDifficulty(currObj, pastObjectDifficultyInfluence, currentVisibleObjectDensity, velocity, constantAngleNerfFactor)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
double preemptDifficulty = calculatePreemptDifficulty(velocity, constantAngleNerfFactor, currObj.Preempt);
|
|
||||||
|
|
||||||
double readingDifficulty = DifficultyCalculationUtils.Norm(1.5, preemptDifficulty, hiddenDifficulty, noteDensityDifficulty);
|
|
||||||
|
|
||||||
// Having less time to process information is harder
|
|
||||||
readingDifficulty *= highBpmBonus(currObj.AdjustedDeltaTime);
|
|
||||||
|
|
||||||
return readingDifficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates the density difficulty of the current object and how hard it is to aim it because of it based on:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item><description>cursor velocity to the current object,</description></item>
|
|
||||||
/// <item><description>how many times the current object's angle was repeated,</description></item>
|
|
||||||
/// <item><description>density of objects visible when the current object appears,</description></item>
|
|
||||||
/// <item><description>density of objects visible when the current object needs to be clicked,</description></item>
|
|
||||||
/// /// </list>
|
|
||||||
/// </summary>
|
|
||||||
private static double calculateDensityDifficulty(OsuDifficultyHitObject? nextObj, double velocity, double constantAngleNerfFactor,
|
|
||||||
double pastObjectDifficultyInfluence, double currentVisibleObjectDensity)
|
|
||||||
{
|
|
||||||
// Consider future densities too because it can make the path the cursor takes less clear
|
|
||||||
double futureObjectDifficultyInfluence = Math.Sqrt(currentVisibleObjectDensity);
|
|
||||||
|
|
||||||
if (nextObj != null)
|
|
||||||
{
|
|
||||||
// Reduce difficulty if movement to next object is small
|
|
||||||
futureObjectDifficultyInfluence *= DifficultyCalculationUtils.Smootherstep(nextObj.LazyJumpDistance, 15, distance_influence_threshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value higher note densities exponentially
|
|
||||||
double noteDensityDifficulty = Math.Pow(pastObjectDifficultyInfluence + futureObjectDifficultyInfluence, 1.7) * 0.4 * constantAngleNerfFactor * velocity;
|
|
||||||
|
|
||||||
// Award only denser than average maps.
|
|
||||||
noteDensityDifficulty = Math.Max(0, noteDensityDifficulty - density_difficulty_base);
|
|
||||||
|
|
||||||
// Apply a soft cap to general density reading to account for partial memorization
|
|
||||||
noteDensityDifficulty = Math.Pow(noteDensityDifficulty, 0.45) * density_multiplier;
|
|
||||||
|
|
||||||
return noteDensityDifficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates the difficulty of aiming the current object when the approach rate is very high based on:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item><description>cursor velocity to the current object,</description></item>
|
|
||||||
/// <item><description>how many times the current object's angle was repeated,</description></item>
|
|
||||||
/// <item><description>how many milliseconds elapse between the approach circle appearing and touching the inner circle</description></item>
|
|
||||||
/// </list>
|
|
||||||
/// </summary>
|
|
||||||
private static double calculatePreemptDifficulty(double velocity, double constantAngleNerfFactor, double preempt)
|
|
||||||
{
|
|
||||||
// Arbitrary curve for the base value preempt difficulty should have as approach rate increases.
|
|
||||||
// https://www.desmos.com/calculator/c175335a71
|
|
||||||
double preemptDifficulty = Math.Pow((preempt_starting_point - preempt + Math.Abs(preempt - preempt_starting_point)) / 2, 2.5) / preempt_balancing_factor;
|
|
||||||
|
|
||||||
preemptDifficulty *= constantAngleNerfFactor * velocity;
|
|
||||||
|
|
||||||
return preemptDifficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates the difficulty of aiming the current object when the hidden mod is active based on:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item><description>cursor velocity to the current object,</description></item>
|
|
||||||
/// <item><description>time the current object spends invisible,</description></item>
|
|
||||||
/// <item><description>density of objects visible when the current object appears,</description></item>
|
|
||||||
/// <item><description>density of objects visible when the current object needs to be clicked,</description></item>
|
|
||||||
/// <item><description>how many times the current object's angle was repeated,</description></item>
|
|
||||||
/// <item><description>if the current object is perfectly stacked to the previous one</description></item>
|
|
||||||
/// </list>
|
|
||||||
/// </summary>
|
|
||||||
private static double calculateHiddenDifficulty(OsuDifficultyHitObject currObj, double pastObjectDifficultyInfluence, double currentVisibleObjectDensity, double velocity,
|
|
||||||
double constantAngleNerfFactor)
|
|
||||||
{
|
|
||||||
// Higher preempt means that time spent invisible is higher too, we want to reward that
|
|
||||||
double preemptFactor = Math.Pow(currObj.Preempt, 2.2) * 0.01;
|
|
||||||
|
|
||||||
// Account for both past and current densities
|
|
||||||
double densityFactor = Math.Pow(currentVisibleObjectDensity + pastObjectDifficultyInfluence, 3.3) * 3;
|
|
||||||
|
|
||||||
double hiddenDifficulty = (preemptFactor + densityFactor) * constantAngleNerfFactor * velocity * 0.01;
|
|
||||||
|
|
||||||
// Apply a soft cap to general HD reading to account for partial memorization
|
|
||||||
hiddenDifficulty = Math.Pow(hiddenDifficulty, 0.4) * hidden_multiplier;
|
|
||||||
|
|
||||||
var previousObj = (OsuDifficultyHitObject)currObj.Previous(0);
|
|
||||||
|
|
||||||
// Buff perfect stacks only if current note is completely invisible at the time you click the previous note.
|
|
||||||
if (currObj.LazyJumpDistance == 0 && currObj.OpacityAt(previousObj.BaseObject.StartTime, true) == 0 && previousObj.StartTime > currObj.StartTime - currObj.Preempt)
|
|
||||||
hiddenDifficulty += hidden_multiplier * 2500 / Math.Pow(currObj.AdjustedDeltaTime, 1.5); // Perfect stacks are harder the less time between notes
|
|
||||||
|
|
||||||
return hiddenDifficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double getPastObjectDifficultyInfluence(OsuDifficultyHitObject currObj)
|
|
||||||
{
|
|
||||||
double pastObjectDifficultyInfluence = 0;
|
|
||||||
|
|
||||||
foreach (var loopObj in retrievePastVisibleObjects(currObj))
|
|
||||||
{
|
|
||||||
double loopDifficulty = currObj.OpacityAt(loopObj.BaseObject.StartTime, false);
|
|
||||||
|
|
||||||
// When aiming an object small distances mean previous objects may be cheesed, so it doesn't matter whether they were arranged confusingly.
|
|
||||||
loopDifficulty *= DifficultyCalculationUtils.Smootherstep(loopObj.LazyJumpDistance, 15, distance_influence_threshold);
|
|
||||||
|
|
||||||
// Account less for objects close to the max reading window
|
|
||||||
double timeBetweenCurrAndLoopObj = currObj.StartTime - loopObj.StartTime;
|
|
||||||
double timeNerfFactor = getTimeNerfFactor(timeBetweenCurrAndLoopObj);
|
|
||||||
|
|
||||||
loopDifficulty *= timeNerfFactor;
|
|
||||||
pastObjectDifficultyInfluence += loopDifficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pastObjectDifficultyInfluence;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a list of objects that are visible on screen at the point in time the current object becomes visible.
|
|
||||||
private static IEnumerable<OsuDifficultyHitObject> retrievePastVisibleObjects(OsuDifficultyHitObject current)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < current.Index; i++)
|
|
||||||
{
|
|
||||||
OsuDifficultyHitObject hitObject = (OsuDifficultyHitObject)current.Previous(i);
|
|
||||||
|
|
||||||
if (hitObject.IsNull() ||
|
|
||||||
current.StartTime - hitObject.StartTime > reading_window_size ||
|
|
||||||
hitObject.StartTime < current.StartTime - current.Preempt) // Current object not visible at the time object needs to be clicked
|
|
||||||
break;
|
|
||||||
|
|
||||||
yield return hitObject;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the density of objects visible at the point in time the current object needs to be clicked capped by the reading window.
|
|
||||||
private static double retrieveCurrentVisibleObjectDensity(OsuDifficultyHitObject current)
|
|
||||||
{
|
|
||||||
double visibleObjectCount = 0;
|
|
||||||
|
|
||||||
OsuDifficultyHitObject? hitObject = (OsuDifficultyHitObject)current.Next(0);
|
|
||||||
|
|
||||||
while (hitObject != null)
|
|
||||||
{
|
|
||||||
if (hitObject.StartTime - current.StartTime > reading_window_size ||
|
|
||||||
current.StartTime < hitObject.StartTime - hitObject.Preempt) // Object not visible at the time current object needs to be clicked.
|
|
||||||
break;
|
|
||||||
|
|
||||||
double timeBetweenCurrAndLoopObj = hitObject.StartTime - current.StartTime;
|
|
||||||
double timeNerfFactor = getTimeNerfFactor(timeBetweenCurrAndLoopObj);
|
|
||||||
|
|
||||||
visibleObjectCount += hitObject.OpacityAt(current.BaseObject.StartTime, false) * timeNerfFactor;
|
|
||||||
|
|
||||||
hitObject = (OsuDifficultyHitObject?)hitObject.Next(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return visibleObjectCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a factor of how often the current object's angle has been repeated in a certain time frame.
|
|
||||||
// It does this by checking the difference in angle between current and past objects and sums them based on a range of similarity.
|
|
||||||
// https://www.desmos.com/calculator/eb057a4822
|
|
||||||
private static double getConstantAngleNerfFactor(OsuDifficultyHitObject current)
|
|
||||||
{
|
|
||||||
double constantAngleCount = 0;
|
|
||||||
int index = 0;
|
|
||||||
double currentTimeGap = 0;
|
|
||||||
|
|
||||||
OsuDifficultyHitObject loopObjPrev0 = current;
|
|
||||||
OsuDifficultyHitObject? loopObjPrev1 = null;
|
|
||||||
OsuDifficultyHitObject? loopObjPrev2 = null;
|
|
||||||
|
|
||||||
while (currentTimeGap < minimum_angle_relevancy_time)
|
|
||||||
{
|
|
||||||
var loopObj = (OsuDifficultyHitObject)current.Previous(index);
|
|
||||||
|
|
||||||
if (loopObj.IsNull())
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Account less for objects that are close to the time limit.
|
|
||||||
double longIntervalFactor = 1 - DifficultyCalculationUtils.ReverseLerp(loopObj.AdjustedDeltaTime, maximum_angle_relevancy_time, minimum_angle_relevancy_time);
|
|
||||||
|
|
||||||
if (loopObj.Angle.IsNotNull() && current.Angle.IsNotNull())
|
|
||||||
{
|
|
||||||
double angleDifference = Math.Abs(current.Angle.Value - loopObj.Angle.Value);
|
|
||||||
double angleDifferenceAlternating = Math.PI;
|
|
||||||
|
|
||||||
if (loopObjPrev0.Angle != null && loopObjPrev1?.Angle != null && loopObjPrev2?.Angle != null)
|
|
||||||
{
|
|
||||||
angleDifferenceAlternating = Math.Abs(loopObjPrev1.Angle.Value - loopObj.Angle.Value);
|
|
||||||
angleDifferenceAlternating += Math.Abs(loopObjPrev2.Angle.Value - loopObjPrev0.Angle.Value);
|
|
||||||
|
|
||||||
double weight = 1.0;
|
|
||||||
|
|
||||||
// Be sure that one of the angles is very sharp, when other is wide
|
|
||||||
weight *= DifficultyCalculationUtils.ReverseLerp(Math.Min(loopObj.Angle.Value, loopObjPrev0.Angle.Value) * 180 / Math.PI, 20, 5);
|
|
||||||
weight *= DifficultyCalculationUtils.ReverseLerp(Math.Max(loopObj.Angle.Value, loopObjPrev0.Angle.Value) * 180 / Math.PI, 60, 120);
|
|
||||||
|
|
||||||
// Lerp between max angle difference and rescaled alternating difference, with more harsh scaling compared to normal difference
|
|
||||||
angleDifferenceAlternating = double.Lerp(Math.PI, 0.1 * angleDifferenceAlternating, weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
double stackFactor = DifficultyCalculationUtils.Smootherstep(loopObj.LazyJumpDistance, 0, OsuDifficultyHitObject.NORMALISED_RADIUS);
|
|
||||||
|
|
||||||
constantAngleCount += Math.Cos(3 * Math.Min(double.DegreesToRadians(30), Math.Min(angleDifference, angleDifferenceAlternating) * stackFactor)) * longIntervalFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentTimeGap = current.StartTime - loopObj.StartTime;
|
|
||||||
index++;
|
|
||||||
|
|
||||||
loopObjPrev2 = loopObjPrev1;
|
|
||||||
loopObjPrev1 = loopObjPrev0;
|
|
||||||
loopObjPrev0 = loopObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.Clamp(2 / constantAngleCount, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a nerfing factor for when objects are very distant in time, affecting reading less.
|
|
||||||
private static double getTimeNerfFactor(double deltaTime)
|
|
||||||
{
|
|
||||||
return Math.Clamp(2 - deltaTime / (reading_window_size / 2), 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double highBpmBonus(double ms) => 1 / (1 - Math.Pow(0.8, ms / 1000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+32
-62
@@ -8,16 +8,15 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
|
|||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
using osu.Game.Rulesets.Difficulty.Utils;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Scoring;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed
|
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||||
{
|
{
|
||||||
public static class RhythmEvaluator
|
public static class RhythmEvaluator
|
||||||
{
|
{
|
||||||
private const int history_time_max = 5 * 1000; // 5 seconds
|
private const int history_time_max = 5 * 1000; // 5 seconds
|
||||||
private const int history_objects_max = 32;
|
private const int history_objects_max = 32;
|
||||||
private const double rhythm_overall_multiplier = 0.95;
|
private const double rhythm_overall_multiplier = 1.0;
|
||||||
private const double rhythm_ratio_multiplier = 26.0;
|
private const double rhythm_ratio_multiplier = 15.0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
|
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
|
||||||
@@ -27,9 +26,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed
|
|||||||
if (current.BaseObject is Spinner)
|
if (current.BaseObject is Spinner)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
var currentOsuObject = (OsuDifficultyHitObject)current;
|
||||||
|
|
||||||
double rhythmComplexitySum = 0;
|
double rhythmComplexitySum = 0;
|
||||||
|
|
||||||
double deltaDifferenceEpsilon = ((OsuDifficultyHitObject)current).HitWindow(HitResult.Great) * 0.3;
|
double deltaDifferenceEpsilon = ((OsuDifficultyHitObject)current).HitWindowGreat * 0.3;
|
||||||
|
|
||||||
var island = new Island(deltaDifferenceEpsilon);
|
var island = new Island(deltaDifferenceEpsilon);
|
||||||
var previousIsland = new Island(deltaDifferenceEpsilon);
|
var previousIsland = new Island(deltaDifferenceEpsilon);
|
||||||
@@ -56,8 +57,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed
|
|||||||
for (int i = rhythmStart; i > 0; i--)
|
for (int i = rhythmStart; i > 0; i--)
|
||||||
{
|
{
|
||||||
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1);
|
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1);
|
||||||
if (currObj.BaseObject is Spinner)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// scales note 0 to 1 from history to now
|
// scales note 0 to 1 from history to now
|
||||||
double timeDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max;
|
double timeDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max;
|
||||||
@@ -65,56 +64,44 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed
|
|||||||
|
|
||||||
double currHistoricalDecay = Math.Min(noteDecay, timeDecay); // either we're limited by time or limited by object count.
|
double currHistoricalDecay = Math.Min(noteDecay, timeDecay); // either we're limited by time or limited by object count.
|
||||||
|
|
||||||
// Use custom cap value to ensure that at this point delta time is actually zero
|
// Use custom cap value to ensure that that at this point delta time is actually zero
|
||||||
double currDelta = Math.Max(currObj.DeltaTime, 1e-7);
|
double currDelta = Math.Max(currObj.DeltaTime, 1e-7);
|
||||||
double prevDelta = Math.Max(prevObj.DeltaTime, 1e-7);
|
double prevDelta = Math.Max(prevObj.DeltaTime, 1e-7);
|
||||||
double lastDelta = Math.Max(lastObj.DeltaTime, 1e-7);
|
double lastDelta = Math.Max(lastObj.DeltaTime, 1e-7);
|
||||||
|
|
||||||
// Make sure to always have the current island initialised - if we don't do it here it will only initialise on the next rhythm change
|
|
||||||
if (island.Delta == int.MaxValue)
|
|
||||||
island = new Island((int)currDelta, deltaDifferenceEpsilon);
|
|
||||||
|
|
||||||
// calculate how much current delta difference deserves a rhythm bonus
|
// calculate how much current delta difference deserves a rhythm bonus
|
||||||
// this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200)
|
// this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200)
|
||||||
double deltaDifference = Math.Max(prevDelta, currDelta) / Math.Min(prevDelta, currDelta);
|
double deltaDifference = Math.Max(prevDelta, currDelta) / Math.Min(prevDelta, currDelta);
|
||||||
|
|
||||||
|
// Take only the fractional part of the value since we're only interested in punishing multiples
|
||||||
|
double deltaDifferenceFraction = deltaDifference - Math.Truncate(deltaDifference);
|
||||||
|
|
||||||
|
double currRatio = 1.0 + rhythm_ratio_multiplier * Math.Min(0.5, DifficultyCalculationUtils.SmoothstepBellCurve(deltaDifferenceFraction));
|
||||||
|
|
||||||
// reduce ratio bonus if delta difference is too big
|
// reduce ratio bonus if delta difference is too big
|
||||||
double differenceMultiplier = Math.Clamp(2.0 - deltaDifference / 8.0, 0.0, 1.0);
|
double differenceMultiplier = Math.Clamp(2.0 - deltaDifference / 8.0, 0.0, 1.0);
|
||||||
|
|
||||||
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon);
|
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon);
|
||||||
|
|
||||||
double effectiveRatio = getEffectiveRatio(deltaDifference) * windowPenalty * differenceMultiplier;
|
double effectiveRatio = windowPenalty * currRatio * differenceMultiplier;
|
||||||
|
|
||||||
// if previous object is a slider it might be easier to tap since you don't have to do a whole tapping motion
|
|
||||||
// while a full deltatime might end up some weird ratio the "unpress->tap" motion might be simple
|
|
||||||
// for example a slider-circle-circle pattern should be evaluated as a regular triple and not as a single->double
|
|
||||||
if (prevObj.BaseObject is Slider)
|
|
||||||
{
|
|
||||||
double sliderLazyEndDelta = currObj.MinimumJumpTime;
|
|
||||||
double sliderLazyDeltaDifference = Math.Max(sliderLazyEndDelta, currDelta) / Math.Min(sliderLazyEndDelta, currDelta);
|
|
||||||
|
|
||||||
double sliderRealEndDelta = currObj.LastObjectEndDeltaTime;
|
|
||||||
double sliderRealDeltaDifference = Math.Max(sliderRealEndDelta, currDelta) / Math.Min(sliderRealEndDelta, currDelta);
|
|
||||||
|
|
||||||
double sliderEffectiveRatio = Math.Min(getEffectiveRatio(sliderLazyDeltaDifference), getEffectiveRatio(sliderRealDeltaDifference));
|
|
||||||
effectiveRatio = Math.Min(sliderEffectiveRatio, effectiveRatio);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isSpeedingUp = prevDelta > currDelta + deltaDifferenceEpsilon;
|
|
||||||
|
|
||||||
if (Math.Abs(prevDelta - currDelta) < deltaDifferenceEpsilon)
|
|
||||||
{
|
|
||||||
// island is still progressing
|
|
||||||
island.AddDelta((int)currDelta);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstDeltaSwitch)
|
if (firstDeltaSwitch)
|
||||||
{
|
{
|
||||||
if (Math.Abs(prevDelta - currDelta) > deltaDifferenceEpsilon)
|
if (Math.Abs(prevDelta - currDelta) < deltaDifferenceEpsilon)
|
||||||
|
{
|
||||||
|
// island is still progressing
|
||||||
|
island.AddDelta((int)currDelta);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
// bpm change is into slider, this is easy acc window
|
// bpm change is into slider, this is easy acc window
|
||||||
if (currObj.BaseObject is Slider)
|
if (currObj.BaseObject is Slider)
|
||||||
effectiveRatio *= 0.5;
|
effectiveRatio *= 0.125;
|
||||||
|
|
||||||
|
// bpm change was from a slider, this is easier typically than circle -> circle
|
||||||
|
// unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders
|
||||||
|
if (prevObj.BaseObject is Slider)
|
||||||
|
effectiveRatio *= 0.3;
|
||||||
|
|
||||||
// repeated island polarity (2 -> 4, 3 -> 5)
|
// repeated island polarity (2 -> 4, 3 -> 5)
|
||||||
if (island.IsSimilarPolarity(previousIsland))
|
if (island.IsSimilarPolarity(previousIsland))
|
||||||
@@ -129,9 +116,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed
|
|||||||
if (previousIsland.DeltaCount == island.DeltaCount)
|
if (previousIsland.DeltaCount == island.DeltaCount)
|
||||||
effectiveRatio *= 0.5;
|
effectiveRatio *= 0.5;
|
||||||
|
|
||||||
if (isSpeedingUp)
|
|
||||||
effectiveRatio *= 0.65;
|
|
||||||
|
|
||||||
var islandCount = islandCounts.FirstOrDefault(x => x.Island.Equals(island));
|
var islandCount = islandCounts.FirstOrDefault(x => x.Island.Equals(island));
|
||||||
|
|
||||||
if (islandCount != default)
|
if (islandCount != default)
|
||||||
@@ -150,10 +134,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (island.DeltaCount > 0)
|
islandCounts.Add((island, 1));
|
||||||
{
|
|
||||||
islandCounts.Add((island, 1));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// scale down the difficulty if the object is doubletappable
|
// scale down the difficulty if the object is doubletappable
|
||||||
@@ -195,18 +176,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed
|
|||||||
prevObj = currObj;
|
prevObj = currObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the current island is long we don't want the sum to have as big of an effect
|
double rhythmDifficulty = Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
||||||
rhythmComplexitySum *= DifficultyCalculationUtils.ReverseLerp(island.DeltaCount, 22, 3);
|
rhythmDifficulty *= 1 - currentOsuObject.GetDoubletapness((OsuDifficultyHitObject)current.Next(0));
|
||||||
|
|
||||||
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though);
|
return rhythmDifficulty;
|
||||||
}
|
|
||||||
|
|
||||||
private static double getEffectiveRatio(double deltaDifference)
|
|
||||||
{
|
|
||||||
// Take only the fractional part of the value since we're only interested in punishing multiples
|
|
||||||
double deltaDifferenceFraction = deltaDifference - Math.Truncate(deltaDifference);
|
|
||||||
|
|
||||||
return 1.0 + rhythm_ratio_multiplier * Math.Min(0.5, DifficultyCalculationUtils.SmoothstepBellCurve(deltaDifferenceFraction));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Island : IEquatable<Island>
|
private class Island : IEquatable<Island>
|
||||||
@@ -238,12 +211,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed
|
|||||||
|
|
||||||
public bool IsSimilarPolarity(Island other)
|
public bool IsSimilarPolarity(Island other)
|
||||||
{
|
{
|
||||||
// single delta islands shouldn't be compared
|
// TODO: consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple)
|
||||||
if (DeltaCount <= 1 || other.DeltaCount <= 1)
|
// naively adding delta check here breaks _a lot_ of maps because of the flawed ratio calculation
|
||||||
return false;
|
return DeltaCount % 2 == other.DeltaCount % 2;
|
||||||
|
|
||||||
return Math.Abs(Delta - other.Delta) < deltaDifferenceEpsilon &&
|
|
||||||
DeltaCount % 2 == other.DeltaCount % 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals(Island? other)
|
public bool Equals(Island? other)
|
||||||
+29
-10
@@ -2,39 +2,47 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
using osu.Game.Rulesets.Difficulty.Utils;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Scoring;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed
|
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||||
{
|
{
|
||||||
public static class SpeedEvaluator
|
public static class SpeedEvaluator
|
||||||
{
|
{
|
||||||
|
private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
|
||||||
private const double min_speed_bonus = 200; // 200 BPM 1/4th
|
private const double min_speed_bonus = 200; // 200 BPM 1/4th
|
||||||
private const double speed_balancing_factor = 40;
|
private const double speed_balancing_factor = 40;
|
||||||
|
private const double distance_multiplier = 0.8;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Evaluates the difficulty of tapping the current object, based on:
|
/// Evaluates the difficulty of tapping the current object, based on:
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><description>time between pressing the previous and current object,</description></item>
|
/// <item><description>time between pressing the previous and current object,</description></item>
|
||||||
|
/// <item><description>distance between those objects,</description></item>
|
||||||
/// <item><description>and how easily they can be cheesed.</description></item>
|
/// <item><description>and how easily they can be cheesed.</description></item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
public static double EvaluateDifficultyOf(DifficultyHitObject current, IReadOnlyList<Mod> mods)
|
||||||
{
|
{
|
||||||
if (current.BaseObject is Spinner)
|
if (current.BaseObject is Spinner)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
// derive strainTime for calculation
|
||||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||||
|
var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null;
|
||||||
|
|
||||||
double strainTime = osuCurrObj.AdjustedDeltaTime;
|
double strainTime = osuCurrObj.AdjustedDeltaTime;
|
||||||
double doubletapness = 1.0 - osuCurrObj.GetDoubletapness((OsuDifficultyHitObject?)osuCurrObj.Next(0));
|
double doubletapness = 1.0 - osuCurrObj.GetDoubletapness((OsuDifficultyHitObject?)osuCurrObj.Next(0));
|
||||||
|
|
||||||
// Cap deltatime to the OD 300 hitwindow.
|
// Cap deltatime to the OD 300 hitwindow.
|
||||||
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
||||||
strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindow(HitResult.Great)) / 0.93, 0.92, 1);
|
strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindowGreat) / 0.93, 0.92, 1);
|
||||||
|
|
||||||
// speedBonus will be 0.0 for BPM < 200
|
// speedBonus will be 0.0 for BPM < 200
|
||||||
double speedBonus = 0.0;
|
double speedBonus = 0.0;
|
||||||
@@ -43,15 +51,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed
|
|||||||
if (DifficultyCalculationUtils.MillisecondsToBPM(strainTime) > min_speed_bonus)
|
if (DifficultyCalculationUtils.MillisecondsToBPM(strainTime) > min_speed_bonus)
|
||||||
speedBonus = 0.75 * Math.Pow((DifficultyCalculationUtils.BPMToMilliseconds(min_speed_bonus) - strainTime) / speed_balancing_factor, 2);
|
speedBonus = 0.75 * Math.Pow((DifficultyCalculationUtils.BPMToMilliseconds(min_speed_bonus) - strainTime) / speed_balancing_factor, 2);
|
||||||
|
|
||||||
// Base difficulty with all bonuses
|
double travelDistance = osuPrevObj?.TravelDistance ?? 0;
|
||||||
double speedDifficulty = (1 + speedBonus) * 1000 / strainTime;
|
double distance = travelDistance + osuCurrObj.MinimumJumpDistance;
|
||||||
|
|
||||||
speedDifficulty *= highBpmBonus(osuCurrObj.AdjustedDeltaTime);
|
// Cap distance at single_spacing_threshold
|
||||||
|
distance = Math.Min(distance, single_spacing_threshold);
|
||||||
|
|
||||||
|
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
|
||||||
|
double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier;
|
||||||
|
|
||||||
|
// Apply reduced small circle bonus because flow aim difficulty on small circles doesn't scale as hard as jumps
|
||||||
|
distanceBonus *= Math.Sqrt(osuCurrObj.SmallCircleBonus);
|
||||||
|
|
||||||
|
if (mods.OfType<OsuModAutopilot>().Any())
|
||||||
|
distanceBonus = 0;
|
||||||
|
|
||||||
|
// Base difficulty with all bonuses
|
||||||
|
double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime;
|
||||||
|
|
||||||
// Apply penalty if there's doubletappable doubles
|
// Apply penalty if there's doubletappable doubles
|
||||||
return speedDifficulty * doubletapness;
|
return difficulty * doubletapness;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double highBpmBonus(double ms) => 1 / (1 - Math.Pow(0.3, ms / 1000));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,12 +45,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
[JsonProperty("flashlight_difficulty")]
|
[JsonProperty("flashlight_difficulty")]
|
||||||
public double FlashlightDifficulty { get; set; }
|
public double FlashlightDifficulty { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The difficulty corresponding to the reading skill.
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("reading_difficulty")]
|
|
||||||
public double ReadingDifficulty { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Describes how much of <see cref="AimDifficulty"/> is contributed to by hitcircles or sliders.
|
/// Describes how much of <see cref="AimDifficulty"/> is contributed to by hitcircles or sliders.
|
||||||
/// A value closer to 1.0 indicates most of <see cref="AimDifficulty"/> is contributed by hitcircles.
|
/// A value closer to 1.0 indicates most of <see cref="AimDifficulty"/> is contributed by hitcircles.
|
||||||
@@ -81,9 +75,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
[JsonProperty("speed_difficult_strain_count")]
|
[JsonProperty("speed_difficult_strain_count")]
|
||||||
public double SpeedDifficultStrainCount { get; set; }
|
public double SpeedDifficultStrainCount { get; set; }
|
||||||
|
|
||||||
[JsonProperty("reading_difficult_note_count")]
|
|
||||||
public double ReadingDifficultNoteCount { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("nested_score_per_object")]
|
[JsonProperty("nested_score_per_object")]
|
||||||
public double NestedScorePerObject { get; set; }
|
public double NestedScorePerObject { get; set; }
|
||||||
|
|
||||||
@@ -93,6 +84,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
[JsonProperty("maximum_legacy_combo_score")]
|
[JsonProperty("maximum_legacy_combo_score")]
|
||||||
public double MaximumLegacyComboScore { get; set; }
|
public double MaximumLegacyComboScore { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The beatmap's drain rate. This doesn't scale with rate-adjusting mods.
|
||||||
|
/// </summary>
|
||||||
|
public double DrainRate { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The number of hitcircles in the beatmap.
|
/// The number of hitcircles in the beatmap.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -115,7 +111,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
yield return (ATTRIB_ID_AIM, AimDifficulty);
|
yield return (ATTRIB_ID_AIM, AimDifficulty);
|
||||||
yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
|
yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
|
||||||
yield return (ATTRIB_ID_READING, ReadingDifficulty);
|
|
||||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||||
|
|
||||||
if (ShouldSerializeFlashlightDifficulty())
|
if (ShouldSerializeFlashlightDifficulty())
|
||||||
@@ -132,7 +127,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
yield return (ATTRIB_ID_NESTED_SCORE_PER_OBJECT, NestedScorePerObject);
|
yield return (ATTRIB_ID_NESTED_SCORE_PER_OBJECT, NestedScorePerObject);
|
||||||
yield return (ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER, LegacyScoreBaseMultiplier);
|
yield return (ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER, LegacyScoreBaseMultiplier);
|
||||||
yield return (ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE, MaximumLegacyComboScore);
|
yield return (ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE, MaximumLegacyComboScore);
|
||||||
yield return (ATTRIB_ID_READING_DIFFICULT_NOTE_COUNT, ReadingDifficultNoteCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||||
@@ -141,7 +135,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
AimDifficulty = values[ATTRIB_ID_AIM];
|
AimDifficulty = values[ATTRIB_ID_AIM];
|
||||||
SpeedDifficulty = values[ATTRIB_ID_SPEED];
|
SpeedDifficulty = values[ATTRIB_ID_SPEED];
|
||||||
ReadingDifficulty = values[ATTRIB_ID_READING];
|
|
||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||||
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
||||||
@@ -154,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
NestedScorePerObject = values[ATTRIB_ID_NESTED_SCORE_PER_OBJECT];
|
NestedScorePerObject = values[ATTRIB_ID_NESTED_SCORE_PER_OBJECT];
|
||||||
LegacyScoreBaseMultiplier = values[ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER];
|
LegacyScoreBaseMultiplier = values[ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER];
|
||||||
MaximumLegacyComboScore = values[ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE];
|
MaximumLegacyComboScore = values[ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE];
|
||||||
ReadingDifficultNoteCount = values[ATTRIB_ID_READING_DIFFICULT_NOTE_COUNT];
|
DrainRate = onlineInfo.DrainRate;
|
||||||
HitCircleCount = onlineInfo.CircleCount;
|
HitCircleCount = onlineInfo.CircleCount;
|
||||||
SliderCount = onlineInfo.SliderCount;
|
SliderCount = onlineInfo.SliderCount;
|
||||||
SpinnerCount = onlineInfo.SpinnerCount;
|
SpinnerCount = onlineInfo.SpinnerCount;
|
||||||
|
|||||||
@@ -8,19 +8,21 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Utils;
|
using osu.Game.Rulesets.Osu.Difficulty.Utils;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||||
{
|
{
|
||||||
public class OsuDifficultyCalculator : DifficultyCalculator
|
public class OsuDifficultyCalculator : DifficultyCalculator
|
||||||
{
|
{
|
||||||
|
private const double star_rating_multiplier = 0.0265;
|
||||||
|
|
||||||
public override int Version => 20251020;
|
public override int Version => 20251020;
|
||||||
|
|
||||||
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
@@ -28,7 +30,23 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills)
|
public static double CalculateRateAdjustedApproachRate(double approachRate, double clockRate)
|
||||||
|
{
|
||||||
|
double preempt = IBeatmapDifficultyInfo.DifficultyRange(approachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN) / clockRate;
|
||||||
|
return IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double CalculateRateAdjustedOverallDifficulty(double overallDifficulty, double clockRate)
|
||||||
|
{
|
||||||
|
HitWindows hitWindows = new OsuHitWindows();
|
||||||
|
hitWindows.SetDifficulty(overallDifficulty);
|
||||||
|
|
||||||
|
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||||
|
|
||||||
|
return (79.5 - hitWindowGreat) / 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
{
|
{
|
||||||
if (beatmap.HitObjects.Count == 0)
|
if (beatmap.HitObjects.Count == 0)
|
||||||
return new OsuDifficultyAttributes { Mods = mods };
|
return new OsuDifficultyAttributes { Mods = mods };
|
||||||
@@ -37,61 +55,66 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
var aimWithoutSliders = skills.OfType<Aim>().Single(a => !a.IncludeSliders);
|
var aimWithoutSliders = skills.OfType<Aim>().Single(a => !a.IncludeSliders);
|
||||||
var speed = skills.OfType<Speed>().Single();
|
var speed = skills.OfType<Speed>().Single();
|
||||||
var flashlight = skills.OfType<Flashlight>().SingleOrDefault();
|
var flashlight = skills.OfType<Flashlight>().SingleOrDefault();
|
||||||
var reading = skills.OfType<Reading>().Single();
|
|
||||||
|
|
||||||
double aimDifficultyValue = aim.DifficultyValue();
|
|
||||||
double aimNoSlidersDifficultyValue = aimWithoutSliders.DifficultyValue();
|
|
||||||
double speedDifficultyValue = speed.DifficultyValue();
|
|
||||||
double readingDifficultyValue = reading.DifficultyValue();
|
|
||||||
|
|
||||||
double aimDifficultStrainCount = aim.CountTopWeightedStrains(aimDifficultyValue);
|
|
||||||
double speedDifficultStrainCount = speed.CountTopWeightedObjectDifficulties(speedDifficultyValue);
|
|
||||||
double readingDifficultNoteCount = reading.CountTopWeightedObjectDifficulties(readingDifficultyValue);
|
|
||||||
|
|
||||||
double speedNotes = speed.RelevantNoteCount();
|
double speedNotes = speed.RelevantNoteCount();
|
||||||
|
|
||||||
double aimNoSlidersTopWeightedSliderCount = aimWithoutSliders.CountTopWeightedSliders(aimNoSlidersDifficultyValue);
|
double aimDifficultStrainCount = aim.CountTopWeightedStrains();
|
||||||
double aimNoSlidersDifficultStrainCount = aimWithoutSliders.CountTopWeightedStrains(aimNoSlidersDifficultyValue);
|
double speedDifficultStrainCount = speed.CountTopWeightedStrains();
|
||||||
|
|
||||||
|
double aimNoSlidersTopWeightedSliderCount = aimWithoutSliders.CountTopWeightedSliders();
|
||||||
|
double aimNoSlidersDifficultStrainCount = aimWithoutSliders.CountTopWeightedStrains();
|
||||||
|
|
||||||
double aimTopWeightedSliderFactor = aimNoSlidersTopWeightedSliderCount / Math.Max(1, aimNoSlidersDifficultStrainCount - aimNoSlidersTopWeightedSliderCount);
|
double aimTopWeightedSliderFactor = aimNoSlidersTopWeightedSliderCount / Math.Max(1, aimNoSlidersDifficultStrainCount - aimNoSlidersTopWeightedSliderCount);
|
||||||
|
|
||||||
double speedTopWeightedSliderCount = speed.CountTopWeightedSliders(speedDifficultyValue);
|
double speedTopWeightedSliderCount = speed.CountTopWeightedSliders();
|
||||||
double speedTopWeightedSliderFactor = speedTopWeightedSliderCount / Math.Max(1, speedDifficultStrainCount - speedTopWeightedSliderCount);
|
double speedTopWeightedSliderFactor = speedTopWeightedSliderCount / Math.Max(1, speedDifficultStrainCount - speedTopWeightedSliderCount);
|
||||||
|
|
||||||
double difficultSliders = aim.GetDifficultSliders();
|
double difficultSliders = aim.GetDifficultSliders();
|
||||||
|
|
||||||
|
double approachRate = CalculateRateAdjustedApproachRate(beatmap.Difficulty.ApproachRate, clockRate);
|
||||||
|
double overallDifficulty = CalculateRateAdjustedOverallDifficulty(beatmap.Difficulty.OverallDifficulty, clockRate);
|
||||||
|
|
||||||
int hitCircleCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
int hitCircleCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
||||||
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
||||||
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
|
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
|
||||||
|
|
||||||
int totalHits = beatmap.HitObjects.Count;
|
int totalHits = beatmap.HitObjects.Count;
|
||||||
|
|
||||||
double sliderFactor = aimDifficultyValue > 0
|
double drainRate = beatmap.Difficulty.DrainRate;
|
||||||
? calculateAimDifficultyRating(aimNoSlidersDifficultyValue) / calculateAimDifficultyRating(aimDifficultyValue)
|
|
||||||
: 1;
|
|
||||||
|
|
||||||
double aimRating = calculateAimDifficultyRating(aimDifficultyValue);
|
double aimDifficultyValue = aim.DifficultyValue();
|
||||||
double speedRating = calculateDifficultyRating(speedDifficultyValue);
|
double aimNoSlidersDifficultyValue = aimWithoutSliders.DifficultyValue();
|
||||||
double readingRating = calculateDifficultyRating(readingDifficultyValue);
|
double speedDifficultyValue = speed.DifficultyValue();
|
||||||
|
|
||||||
|
double mechanicalDifficultyRating = calculateMechanicalDifficultyRating(aimDifficultyValue, speedDifficultyValue);
|
||||||
|
double sliderFactor = aimDifficultyValue > 0 ? OsuRatingCalculator.CalculateDifficultyRating(aimNoSlidersDifficultyValue) / OsuRatingCalculator.CalculateDifficultyRating(aimDifficultyValue) : 1;
|
||||||
|
|
||||||
|
var osuRatingCalculator = new OsuRatingCalculator(mods, totalHits, approachRate, overallDifficulty, mechanicalDifficultyRating, sliderFactor);
|
||||||
|
|
||||||
|
double aimRating = osuRatingCalculator.ComputeAimRating(aimDifficultyValue);
|
||||||
|
double speedRating = osuRatingCalculator.ComputeSpeedRating(speedDifficultyValue);
|
||||||
|
|
||||||
double flashlightRating = 0.0;
|
double flashlightRating = 0.0;
|
||||||
|
|
||||||
if (flashlight is not null)
|
if (flashlight is not null)
|
||||||
flashlightRating = calculateDifficultyRating(flashlight.DifficultyValue());
|
flashlightRating = osuRatingCalculator.ComputeFlashlightRating(flashlight.DifficultyValue());
|
||||||
|
|
||||||
double sliderNestedScorePerObject = LegacyScoreUtils.CalculateNestedScorePerObject(beatmap, totalHits);
|
double sliderNestedScorePerObject = LegacyScoreUtils.CalculateNestedScorePerObject(beatmap, totalHits);
|
||||||
double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(WorkingBeatmap.Beatmap);
|
double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(beatmap);
|
||||||
|
|
||||||
var simulator = new OsuLegacyScoreSimulator();
|
var simulator = new OsuLegacyScoreSimulator();
|
||||||
var scoreAttributes = simulator.Simulate(WorkingBeatmap, beatmap);
|
var scoreAttributes = simulator.Simulate(WorkingBeatmap, beatmap);
|
||||||
|
|
||||||
double baseAimPerformance = OsuPerformanceCalculator.DifficultyToPerformance(aimRating);
|
double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
|
||||||
double baseSpeedPerformance = HarmonicSkill.DifficultyToPerformance(speedRating);
|
double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);
|
||||||
double baseReadingPerformance = HarmonicSkill.DifficultyToPerformance(readingRating);
|
|
||||||
double baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating);
|
double baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating);
|
||||||
double baseCognitionPerformance = SumCognitionDifficulty(baseReadingPerformance, baseFlashlightPerformance);
|
|
||||||
|
|
||||||
double basePerformance = DifficultyCalculationUtils.Norm(OsuPerformanceCalculator.PERFORMANCE_NORM_EXPONENT, baseAimPerformance, baseSpeedPerformance, baseCognitionPerformance);
|
double basePerformance =
|
||||||
|
Math.Pow(
|
||||||
|
Math.Pow(baseAimPerformance, 1.1) +
|
||||||
|
Math.Pow(baseSpeedPerformance, 1.1) +
|
||||||
|
Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
|
||||||
|
);
|
||||||
|
|
||||||
double starRating = calculateStarRating(basePerformance);
|
double starRating = calculateStarRating(basePerformance);
|
||||||
|
|
||||||
@@ -104,13 +127,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
SpeedDifficulty = speedRating,
|
SpeedDifficulty = speedRating,
|
||||||
SpeedNoteCount = speedNotes,
|
SpeedNoteCount = speedNotes,
|
||||||
FlashlightDifficulty = flashlightRating,
|
FlashlightDifficulty = flashlightRating,
|
||||||
ReadingDifficulty = readingRating,
|
|
||||||
SliderFactor = sliderFactor,
|
SliderFactor = sliderFactor,
|
||||||
AimDifficultStrainCount = aimDifficultStrainCount,
|
AimDifficultStrainCount = aimDifficultStrainCount,
|
||||||
SpeedDifficultStrainCount = speedDifficultStrainCount,
|
SpeedDifficultStrainCount = speedDifficultStrainCount,
|
||||||
ReadingDifficultNoteCount = readingDifficultNoteCount,
|
|
||||||
AimTopWeightedSliderFactor = aimTopWeightedSliderFactor,
|
AimTopWeightedSliderFactor = aimTopWeightedSliderFactor,
|
||||||
SpeedTopWeightedSliderFactor = speedTopWeightedSliderFactor,
|
SpeedTopWeightedSliderFactor = speedTopWeightedSliderFactor,
|
||||||
|
DrainRate = drainRate,
|
||||||
MaxCombo = beatmap.GetMaxCombo(),
|
MaxCombo = beatmap.GetMaxCombo(),
|
||||||
HitCircleCount = hitCircleCount,
|
HitCircleCount = hitCircleCount,
|
||||||
SliderCount = sliderCount,
|
SliderCount = sliderCount,
|
||||||
@@ -123,33 +145,28 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static double SumCognitionDifficulty(double reading, double flashlight)
|
private double calculateMechanicalDifficultyRating(double aimDifficultyValue, double speedDifficultyValue)
|
||||||
{
|
{
|
||||||
if (reading <= 0)
|
double aimValue = OsuStrainSkill.DifficultyToPerformance(OsuRatingCalculator.CalculateDifficultyRating(aimDifficultyValue));
|
||||||
return flashlight;
|
double speedValue = OsuStrainSkill.DifficultyToPerformance(OsuRatingCalculator.CalculateDifficultyRating(speedDifficultyValue));
|
||||||
|
|
||||||
if (flashlight <= 0)
|
double totalValue = Math.Pow(Math.Pow(aimValue, 1.1) + Math.Pow(speedValue, 1.1), 1 / 1.1);
|
||||||
return reading;
|
|
||||||
|
|
||||||
// Nerf flashlight value in cognition sum when reading is greater than flashlight
|
return calculateStarRating(totalValue);
|
||||||
return DifficultyCalculationUtils.Norm(OsuPerformanceCalculator.PERFORMANCE_NORM_EXPONENT, reading, flashlight * Math.Clamp(flashlight / reading, 0.25, 1.0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private double calculateAimDifficultyRating(double difficultyValue) => Math.Pow(difficultyValue, 0.63) * 0.02275;
|
|
||||||
|
|
||||||
private double calculateDifficultyRating(double difficultyValue) => Math.Sqrt(difficultyValue) * 0.0675;
|
|
||||||
|
|
||||||
private double calculateStarRating(double basePerformance)
|
private double calculateStarRating(double basePerformance)
|
||||||
{
|
{
|
||||||
return Math.Cbrt(basePerformance * OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER);
|
if (basePerformance <= 0.00001)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * star_rating_multiplier * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, Mod[] mods)
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
{
|
{
|
||||||
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
||||||
|
|
||||||
double clockRate = ModUtils.CalculateRateWithMods(mods);
|
|
||||||
|
|
||||||
// The first jump is formed by the first two hitobjects of the map.
|
// The first jump is formed by the first two hitobjects of the map.
|
||||||
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
|
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
|
||||||
for (int i = 1; i < beatmap.HitObjects.Count; i++)
|
for (int i = 1; i < beatmap.HitObjects.Count; i++)
|
||||||
@@ -160,18 +177,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
return objects;
|
return objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||||
{
|
{
|
||||||
var skills = new List<Skill>
|
var skills = new List<Skill>
|
||||||
{
|
{
|
||||||
new Aim(mods, true),
|
new Aim(mods, true),
|
||||||
new Aim(mods, false),
|
new Aim(mods, false),
|
||||||
new Speed(mods),
|
new Speed(mods)
|
||||||
new Reading(mods)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mods.Any(h => h is OsuModFlashlight))
|
if (mods.Any(h => h is OsuModFlashlight))
|
||||||
skills.Add(new Flashlight(mods, beatmap.HitObjects.Count));
|
skills.Add(new Flashlight(mods));
|
||||||
|
|
||||||
return skills.ToArray();
|
return skills.ToArray();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,13 +115,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
double missCount = 0;
|
double missCount = 0;
|
||||||
|
|
||||||
// If sliders in the map are hard - it's likely for player to drop sliderends
|
|
||||||
// If map has easy sliders - it's more likely for player to sliderbreak
|
|
||||||
double likelyMissedSliderendPortion = 0.04 + 0.06 * Math.Pow(Math.Min(attributes.AimTopWeightedSliderFactor, 1), 2);
|
|
||||||
|
|
||||||
// Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
|
// Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
|
||||||
// In classic scores we can't know the amount of dropped sliders so we estimate it
|
// In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
|
||||||
double fullComboThreshold = attributes.MaxCombo - Math.Min(4 + likelyMissedSliderendPortion * attributes.SliderCount, attributes.SliderCount);
|
double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
|
||||||
|
|
||||||
if (score.MaxCombo < fullComboThreshold)
|
if (score.MaxCombo < fullComboThreshold)
|
||||||
missCount = Math.Pow(fullComboThreshold / Math.Max(1.0, score.MaxCombo), 2.5);
|
missCount = Math.Pow(fullComboThreshold / Math.Max(1.0, score.MaxCombo), 2.5);
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
[JsonProperty("flashlight")]
|
[JsonProperty("flashlight")]
|
||||||
public double Flashlight { get; set; }
|
public double Flashlight { get; set; }
|
||||||
|
|
||||||
[JsonProperty("reading")]
|
|
||||||
public double Reading { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("effective_miss_count")]
|
[JsonProperty("effective_miss_count")]
|
||||||
public double EffectiveMissCount { get; set; }
|
public double EffectiveMissCount { get; set; }
|
||||||
|
|
||||||
@@ -51,7 +48,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
yield return new PerformanceDisplayAttribute(nameof(Speed), "Speed", Speed);
|
yield return new PerformanceDisplayAttribute(nameof(Speed), "Speed", Speed);
|
||||||
yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy);
|
yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy);
|
||||||
yield return new PerformanceDisplayAttribute(nameof(Flashlight), "Flashlight Bonus", Flashlight);
|
yield return new PerformanceDisplayAttribute(nameof(Flashlight), "Flashlight Bonus", Flashlight);
|
||||||
yield return new PerformanceDisplayAttribute(nameof(Reading), "Reading", Reading);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,12 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Rulesets.Difficulty;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
using osu.Game.Rulesets.Difficulty.Utils;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
using osu.Game.Rulesets.Osu.Scoring;
|
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
@@ -22,8 +19,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
{
|
{
|
||||||
public class OsuPerformanceCalculator : PerformanceCalculator
|
public class OsuPerformanceCalculator : PerformanceCalculator
|
||||||
{
|
{
|
||||||
public const double PERFORMANCE_BASE_MULTIPLIER = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||||
public const double PERFORMANCE_NORM_EXPONENT = 1.1;
|
|
||||||
|
|
||||||
private bool usingClassicSliderAccuracy;
|
private bool usingClassicSliderAccuracy;
|
||||||
private bool usingScoreV2;
|
private bool usingScoreV2;
|
||||||
@@ -54,18 +50,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
private double greatHitWindow;
|
private double greatHitWindow;
|
||||||
private double okHitWindow;
|
private double okHitWindow;
|
||||||
private double mehHitWindow;
|
private double mehHitWindow;
|
||||||
|
|
||||||
private double overallDifficulty;
|
private double overallDifficulty;
|
||||||
private double approachRate;
|
private double approachRate;
|
||||||
private double drainRate;
|
|
||||||
|
|
||||||
private double? speedDeviation;
|
private double? speedDeviation;
|
||||||
|
|
||||||
private double aimEstimatedSliderBreaks;
|
private double aimEstimatedSliderBreaks;
|
||||||
private double speedEstimatedSliderBreaks;
|
private double speedEstimatedSliderBreaks;
|
||||||
|
|
||||||
public static double DifficultyToPerformance(double difficulty) => 4.0 * Math.Pow(difficulty, 3.0);
|
|
||||||
|
|
||||||
public OsuPerformanceCalculator()
|
public OsuPerformanceCalculator()
|
||||||
: base(new OsuRuleset())
|
: base(new OsuRuleset())
|
||||||
{
|
{
|
||||||
@@ -101,14 +93,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
okHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate;
|
okHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate;
|
||||||
mehHitWindow = hitWindows.WindowFor(HitResult.Meh) / clockRate;
|
mehHitWindow = hitWindows.WindowFor(HitResult.Meh) / clockRate;
|
||||||
|
|
||||||
approachRate = calculateRateAdjustedApproachRate(difficulty.ApproachRate, clockRate);
|
approachRate = OsuDifficultyCalculator.CalculateRateAdjustedApproachRate(difficulty.ApproachRate, clockRate);
|
||||||
overallDifficulty = (79.5 - greatHitWindow) / 6;
|
overallDifficulty = OsuDifficultyCalculator.CalculateRateAdjustedOverallDifficulty(difficulty.OverallDifficulty, clockRate);
|
||||||
drainRate = difficulty.DrainRate;
|
|
||||||
|
|
||||||
double comboBasedEstimatedMissCount = calculateComboBasedEstimatedMissCount(osuAttributes);
|
double comboBasedEstimatedMissCount = calculateComboBasedEstimatedMissCount(osuAttributes);
|
||||||
double? scoreBasedEstimatedMissCount = null;
|
double? scoreBasedEstimatedMissCount = null;
|
||||||
|
|
||||||
if (usingClassicSliderAccuracy && !usingScoreV2 && score.LegacyTotalScore != null)
|
if (usingClassicSliderAccuracy && score.LegacyTotalScore != null)
|
||||||
{
|
{
|
||||||
var legacyScoreMissCalculator = new OsuLegacyScoreMissCalculator(score, osuAttributes);
|
var legacyScoreMissCalculator = new OsuLegacyScoreMissCalculator(score, osuAttributes);
|
||||||
scoreBasedEstimatedMissCount = legacyScoreMissCalculator.Calculate();
|
scoreBasedEstimatedMissCount = legacyScoreMissCalculator.Calculate();
|
||||||
@@ -124,12 +115,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
effectiveMissCount = Math.Max(countMiss, effectiveMissCount);
|
effectiveMissCount = Math.Max(countMiss, effectiveMissCount);
|
||||||
effectiveMissCount = Math.Min(totalHits, effectiveMissCount);
|
effectiveMissCount = Math.Min(totalHits, effectiveMissCount);
|
||||||
|
|
||||||
if (effectiveMissCount > 0)
|
|
||||||
{
|
|
||||||
aimEstimatedSliderBreaks = calculateEstimatedSliderBreaks(osuAttributes.AimTopWeightedSliderFactor, osuAttributes);
|
|
||||||
speedEstimatedSliderBreaks = calculateEstimatedSliderBreaks(osuAttributes.SpeedTopWeightedSliderFactor, osuAttributes);
|
|
||||||
}
|
|
||||||
|
|
||||||
double multiplier = PERFORMANCE_BASE_MULTIPLIER;
|
double multiplier = PERFORMANCE_BASE_MULTIPLIER;
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is OsuModNoFail))
|
if (score.Mods.Any(m => m is OsuModNoFail))
|
||||||
@@ -155,12 +140,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
double aimValue = computeAimValue(score, osuAttributes);
|
double aimValue = computeAimValue(score, osuAttributes);
|
||||||
double speedValue = computeSpeedValue(score, osuAttributes);
|
double speedValue = computeSpeedValue(score, osuAttributes);
|
||||||
double accuracyValue = computeAccuracyValue(score, osuAttributes);
|
double accuracyValue = computeAccuracyValue(score, osuAttributes);
|
||||||
|
|
||||||
double readingValue = computeReadingValue(osuAttributes);
|
|
||||||
double flashlightValue = computeFlashlightValue(score, osuAttributes);
|
double flashlightValue = computeFlashlightValue(score, osuAttributes);
|
||||||
double cognitionValue = OsuDifficultyCalculator.SumCognitionDifficulty(readingValue, flashlightValue);
|
|
||||||
|
|
||||||
double totalValue = DifficultyCalculationUtils.Norm(PERFORMANCE_NORM_EXPONENT, aimValue, speedValue, accuracyValue, cognitionValue) * multiplier;
|
double totalValue =
|
||||||
|
Math.Pow(
|
||||||
|
Math.Pow(aimValue, 1.1) +
|
||||||
|
Math.Pow(speedValue, 1.1) +
|
||||||
|
Math.Pow(accuracyValue, 1.1) +
|
||||||
|
Math.Pow(flashlightValue, 1.1), 1.0 / 1.1
|
||||||
|
) * multiplier;
|
||||||
|
|
||||||
return new OsuPerformanceAttributes
|
return new OsuPerformanceAttributes
|
||||||
{
|
{
|
||||||
@@ -168,7 +156,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
Speed = speedValue,
|
Speed = speedValue,
|
||||||
Accuracy = accuracyValue,
|
Accuracy = accuracyValue,
|
||||||
Flashlight = flashlightValue,
|
Flashlight = flashlightValue,
|
||||||
Reading = readingValue,
|
|
||||||
EffectiveMissCount = effectiveMissCount,
|
EffectiveMissCount = effectiveMissCount,
|
||||||
ComboBasedEstimatedMissCount = comboBasedEstimatedMissCount,
|
ComboBasedEstimatedMissCount = comboBasedEstimatedMissCount,
|
||||||
ScoreBasedEstimatedMissCount = scoreBasedEstimatedMissCount,
|
ScoreBasedEstimatedMissCount = scoreBasedEstimatedMissCount,
|
||||||
@@ -207,14 +194,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
aimDifficulty *= sliderNerfFactor;
|
aimDifficulty *= sliderNerfFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
double aimValue = DifficultyToPerformance(aimDifficulty);
|
double aimValue = OsuStrainSkill.DifficultyToPerformance(aimDifficulty);
|
||||||
|
|
||||||
double lengthBonus = 0.95 + 0.35 * Math.Min(1.0, totalHits / 2000.0) +
|
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||||
aimValue *= lengthBonus;
|
aimValue *= lengthBonus;
|
||||||
|
|
||||||
if (effectiveMissCount > 0)
|
if (effectiveMissCount > 0)
|
||||||
{
|
{
|
||||||
|
aimEstimatedSliderBreaks = calculateEstimatedSliderBreaks(attributes.AimTopWeightedSliderFactor, attributes);
|
||||||
|
|
||||||
double relevantMissCount = Math.Min(effectiveMissCount + aimEstimatedSliderBreaks, totalImperfectHits + countSliderTickMiss);
|
double relevantMissCount = Math.Min(effectiveMissCount + aimEstimatedSliderBreaks, totalImperfectHits + countSliderTickMiss);
|
||||||
|
|
||||||
aimValue *= calculateMissPenalty(relevantMissCount, attributes.AimDifficultStrainCount);
|
aimValue *= calculateMissPenalty(relevantMissCount, attributes.AimDifficultStrainCount);
|
||||||
@@ -222,10 +211,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
// TC bonuses are excluded when blinds is present as the increased visual difficulty is unimportant when notes cannot be seen.
|
// TC bonuses are excluded when blinds is present as the increased visual difficulty is unimportant when notes cannot be seen.
|
||||||
if (score.Mods.Any(m => m is OsuModBlinds))
|
if (score.Mods.Any(m => m is OsuModBlinds))
|
||||||
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * drainRate * drainRate);
|
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate);
|
||||||
else if (score.Mods.Any(m => m is OsuModTraceable))
|
else if (score.Mods.Any(m => m is OsuModTraceable))
|
||||||
{
|
{
|
||||||
aimValue *= 1.0 + calculateTraceableBonus(attributes.SliderFactor);
|
aimValue *= 1.0 + OsuRatingCalculator.CalculateVisibilityBonus(score.Mods, approachRate, sliderFactor: attributes.SliderFactor);
|
||||||
}
|
}
|
||||||
|
|
||||||
aimValue *= accuracy;
|
aimValue *= accuracy;
|
||||||
@@ -238,33 +227,44 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
if (score.Mods.Any(h => h is OsuModRelax) || speedDeviation == null)
|
if (score.Mods.Any(h => h is OsuModRelax) || speedDeviation == null)
|
||||||
return 0.0;
|
return 0.0;
|
||||||
|
|
||||||
double speedValue = HarmonicSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||||
|
|
||||||
|
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||||
|
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||||
|
speedValue *= lengthBonus;
|
||||||
|
|
||||||
if (effectiveMissCount > 0)
|
if (effectiveMissCount > 0)
|
||||||
{
|
{
|
||||||
|
speedEstimatedSliderBreaks = calculateEstimatedSliderBreaks(attributes.SpeedTopWeightedSliderFactor, attributes);
|
||||||
|
|
||||||
double relevantMissCount = Math.Min(effectiveMissCount + speedEstimatedSliderBreaks, totalImperfectHits + countSliderTickMiss);
|
double relevantMissCount = Math.Min(effectiveMissCount + speedEstimatedSliderBreaks, totalImperfectHits + countSliderTickMiss);
|
||||||
|
|
||||||
speedValue *= calculateMissPenalty(relevantMissCount, attributes.SpeedDifficultStrainCount);
|
speedValue *= calculateMissPenalty(relevantMissCount, attributes.SpeedDifficultStrainCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TC bonuses are excluded when blinds is present as the increased visual difficulty is unimportant when notes cannot be seen.
|
||||||
if (score.Mods.Any(m => m is OsuModBlinds))
|
if (score.Mods.Any(m => m is OsuModBlinds))
|
||||||
{
|
{
|
||||||
// Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
|
// Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
|
||||||
speedValue *= 1.12;
|
speedValue *= 1.12;
|
||||||
}
|
}
|
||||||
|
else if (score.Mods.Any(m => m is OsuModTraceable))
|
||||||
|
{
|
||||||
|
speedValue *= 1.0 + OsuRatingCalculator.CalculateVisibilityBonus(score.Mods, approachRate);
|
||||||
|
}
|
||||||
|
|
||||||
double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes);
|
double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes);
|
||||||
speedValue *= speedHighDeviationMultiplier;
|
speedValue *= speedHighDeviationMultiplier;
|
||||||
|
|
||||||
// An effective hit window is created based on the speed SR. The higher the speed difficulty, the shorter the hit window.
|
// Calculate accuracy assuming the worst case scenario
|
||||||
// For example, a speed SR of 4.0 leads to an effective hit window of 20ms, which is OD 10.
|
double relevantTotalDiff = Math.Max(0, totalHits - attributes.SpeedNoteCount);
|
||||||
double effectiveHitWindow = 20 * Math.Pow(4 / attributes.SpeedDifficulty, 0.35);
|
double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
|
||||||
|
double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat));
|
||||||
|
double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk));
|
||||||
|
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
|
||||||
|
|
||||||
// Find the proportion of 300s on speed notes assuming the hit window was the effective hit window.
|
// Scale the speed value with accuracy and OD.
|
||||||
double effectiveAccuracy = DifficultyCalculationUtils.Erf(effectiveHitWindow / (double)speedDeviation);
|
speedValue *= Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - overallDifficulty) / 2);
|
||||||
|
|
||||||
// Scale speed value by normalized accuracy.
|
|
||||||
speedValue *= Math.Pow(effectiveAccuracy, 2);
|
|
||||||
|
|
||||||
return speedValue;
|
return speedValue;
|
||||||
}
|
}
|
||||||
@@ -294,19 +294,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
double accuracyValue = Math.Pow(1.52163, overallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
|
double accuracyValue = Math.Pow(1.52163, overallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
|
||||||
|
|
||||||
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
|
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
|
||||||
accuracyValue *= amountHitObjectsWithAccuracy < 1000
|
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
|
||||||
? Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)
|
|
||||||
: Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.1);
|
|
||||||
|
|
||||||
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
|
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
|
||||||
if (score.Mods.Any(m => m is OsuModBlinds))
|
if (score.Mods.Any(m => m is OsuModBlinds))
|
||||||
accuracyValue *= 1.14;
|
accuracyValue *= 1.14;
|
||||||
else if (score.Mods.Any(m => m is OsuModTraceable))
|
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
|
||||||
{
|
{
|
||||||
// Decrease bonus for AR > 10
|
// Decrease bonus for AR > 10
|
||||||
accuracyValue *= 1 + 0.08 * DifficultyCalculationUtils.ReverseLerp(approachRate, 11.5, 10);
|
accuracyValue *= 1 + 0.08 * DifficultyCalculationUtils.ReverseLerp(approachRate, 11.5, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (score.Mods.Any(m => m is OsuModFlashlight))
|
||||||
|
accuracyValue *= 1.02;
|
||||||
|
|
||||||
return accuracyValue;
|
return accuracyValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,19 +330,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
return flashlightValue;
|
return flashlightValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double computeReadingValue(OsuDifficultyAttributes attributes)
|
|
||||||
{
|
|
||||||
double readingValue = HarmonicSkill.DifficultyToPerformance(attributes.ReadingDifficulty);
|
|
||||||
|
|
||||||
if (effectiveMissCount > 0)
|
|
||||||
readingValue *= calculateMissPenalty(effectiveMissCount + aimEstimatedSliderBreaks, attributes.ReadingDifficultNoteCount);
|
|
||||||
|
|
||||||
// Scale the reading value with accuracy _harshly_.
|
|
||||||
readingValue *= Math.Pow(accuracy, 3);
|
|
||||||
|
|
||||||
return readingValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double calculateComboBasedEstimatedMissCount(OsuDifficultyAttributes attributes)
|
private double calculateComboBasedEstimatedMissCount(OsuDifficultyAttributes attributes)
|
||||||
{
|
{
|
||||||
if (attributes.SliderCount <= 0)
|
if (attributes.SliderCount <= 0)
|
||||||
@@ -351,13 +339,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
if (usingClassicSliderAccuracy)
|
if (usingClassicSliderAccuracy)
|
||||||
{
|
{
|
||||||
// If sliders in the map are hard - it's likely for player to drop sliderends
|
|
||||||
// If map has easy sliders - it's more likely for player to sliderbreak
|
|
||||||
double likelyMissedSliderendPortion = 0.04 + 0.06 * Math.Pow(Math.Min(attributes.AimTopWeightedSliderFactor, 1), 2);
|
|
||||||
|
|
||||||
// Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
|
// Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it
|
||||||
// In classic scores we can't know the amount of dropped sliders so we estimate it
|
// In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map
|
||||||
double fullComboThreshold = attributes.MaxCombo - Math.Min(4 + likelyMissedSliderendPortion * attributes.SliderCount, attributes.SliderCount);
|
double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
|
||||||
|
|
||||||
if (scoreMaxCombo < fullComboThreshold)
|
if (scoreMaxCombo < fullComboThreshold)
|
||||||
missCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
missCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
||||||
@@ -392,22 +376,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
private double calculateEstimatedSliderBreaks(double topWeightedSliderFactor, OsuDifficultyAttributes attributes)
|
private double calculateEstimatedSliderBreaks(double topWeightedSliderFactor, OsuDifficultyAttributes attributes)
|
||||||
{
|
{
|
||||||
int nonMissMistakes = countOk + countMeh;
|
if (!usingClassicSliderAccuracy || countOk == 0)
|
||||||
|
|
||||||
if (!usingClassicSliderAccuracy || nonMissMistakes == 0)
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
double missedComboPercent = 1.0 - (double)scoreMaxCombo / attributes.MaxCombo;
|
double missedComboPercent = 1.0 - (double)scoreMaxCombo / attributes.MaxCombo;
|
||||||
double estimatedSliderBreaks = Math.Min(nonMissMistakes, effectiveMissCount * topWeightedSliderFactor);
|
double estimatedSliderBreaks = Math.Min(countOk, effectiveMissCount * topWeightedSliderFactor);
|
||||||
|
|
||||||
// Scores with more Oks and Mehs are more likely to have slider breaks.
|
// Scores with more Oks are more likely to have slider breaks.
|
||||||
// We add an arbitrary value to both sides of the division to make it more stable on extreme ends.
|
double okAdjustment = ((countOk - estimatedSliderBreaks) + 0.5) / countOk;
|
||||||
double nonMissMistakeAdjustment = (nonMissMistakes - estimatedSliderBreaks + 4.5) / (nonMissMistakes + 4);
|
|
||||||
|
|
||||||
// There is a low probability of extra slider breaks on effective miss counts close to 1, as score based calculations are good at indicating if only a single break occurred.
|
// There is a low probability of extra slider breaks on effective miss counts close to 1, as score based calculations are good at indicating if only a single break occurred.
|
||||||
estimatedSliderBreaks *= DifficultyCalculationUtils.Smoothstep(effectiveMissCount, 1, 2);
|
estimatedSliderBreaks *= DifficultyCalculationUtils.Smoothstep(effectiveMissCount, 1, 2);
|
||||||
|
|
||||||
return estimatedSliderBreaks * nonMissMistakeAdjustment * DifficultyCalculationUtils.Logistic(missedComboPercent, 0.33, 15);
|
return estimatedSliderBreaks * okAdjustment * DifficultyCalculationUtils.Logistic(missedComboPercent, 0.33, 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -489,7 +470,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
if (speedDeviation == null)
|
if (speedDeviation == null)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
double speedValue = HarmonicSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||||
|
|
||||||
// Decides a point where the PP value achieved compared to the speed deviation is assumed to be tapped improperly. Any PP above this point is considered "excess" speed difficulty.
|
// Decides a point where the PP value achieved compared to the speed deviation is assumed to be tapped improperly. Any PP above this point is considered "excess" speed difficulty.
|
||||||
// This is used to cause PP above the cutoff to scale logarithmically towards the original speed value thus nerfing the value.
|
// This is used to cause PP above the cutoff to scale logarithmically towards the original speed value thus nerfing the value.
|
||||||
@@ -508,42 +489,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
return adjustedSpeedValue / speedValue;
|
return adjustedSpeedValue / speedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates a visibility bonus that is applicable to Traceable.
|
|
||||||
/// </summary>
|
|
||||||
private double calculateTraceableBonus(double sliderFactor = 1)
|
|
||||||
{
|
|
||||||
// We want to reward slider aim less, more so at lower AR
|
|
||||||
double highApproachRateSliderVisibilityFactor = 0.5 + (Math.Pow(sliderFactor, 6) / 2);
|
|
||||||
double lowApproachRateSliderVisibilityFactor = Math.Pow(sliderFactor, 6);
|
|
||||||
|
|
||||||
// Start from normal curve, rewarding lower AR up to AR7
|
|
||||||
double traceableBonus = 0.0275;
|
|
||||||
traceableBonus += 0.025 * (12.0 - Math.Max(approachRate, 7)) * highApproachRateSliderVisibilityFactor;
|
|
||||||
|
|
||||||
// For AR up to 0 - reduce reward for very low ARs when object is visible
|
|
||||||
if (approachRate < 7)
|
|
||||||
traceableBonus += 0.025 * (7.0 - Math.Max(approachRate, 0)) * lowApproachRateSliderVisibilityFactor;
|
|
||||||
|
|
||||||
// Starting from AR0 - cap values so they won't grow to infinity
|
|
||||||
if (approachRate < 0)
|
|
||||||
traceableBonus += 0.025 * (1 - Math.Pow(1.5, approachRate)) * lowApproachRateSliderVisibilityFactor;
|
|
||||||
|
|
||||||
return traceableBonus;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Miss penalty assumes that a player will miss on the hardest parts of a map,
|
// Miss penalty assumes that a player will miss on the hardest parts of a map,
|
||||||
// so we use the amount of relatively difficult sections to adjust miss penalty
|
// so we use the amount of relatively difficult sections to adjust miss penalty
|
||||||
// to make it more punishing on maps with lower amount of hard sections.
|
// to make it more punishing on maps with lower amount of hard sections.
|
||||||
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.93 / (missCount / (4 * Math.Log(difficultStrainCount)) + 1);
|
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1);
|
||||||
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
|
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
|
||||||
|
|
||||||
private double calculateRateAdjustedApproachRate(double approachRate, double clockRate)
|
|
||||||
{
|
|
||||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(approachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN) / clockRate;
|
|
||||||
return IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||||
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
||||||
private int totalImperfectHits => countOk + countMeh + countMiss;
|
private int totalImperfectHits => countOk + countMeh + countMiss;
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
// 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.Difficulty.Utils;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||||
|
{
|
||||||
|
public class OsuRatingCalculator
|
||||||
|
{
|
||||||
|
private const double difficulty_multiplier = 0.0675;
|
||||||
|
|
||||||
|
private readonly Mod[] mods;
|
||||||
|
private readonly int totalHits;
|
||||||
|
private readonly double approachRate;
|
||||||
|
private readonly double overallDifficulty;
|
||||||
|
private readonly double mechanicalDifficultyRating;
|
||||||
|
private readonly double sliderFactor;
|
||||||
|
|
||||||
|
public OsuRatingCalculator(Mod[] mods, int totalHits, double approachRate, double overallDifficulty, double mechanicalDifficultyRating, double sliderFactor)
|
||||||
|
{
|
||||||
|
this.mods = mods;
|
||||||
|
this.totalHits = totalHits;
|
||||||
|
this.approachRate = approachRate;
|
||||||
|
this.overallDifficulty = overallDifficulty;
|
||||||
|
this.mechanicalDifficultyRating = mechanicalDifficultyRating;
|
||||||
|
this.sliderFactor = sliderFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double ComputeAimRating(double aimDifficultyValue)
|
||||||
|
{
|
||||||
|
if (mods.Any(m => m is OsuModAutopilot))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
double aimRating = CalculateDifficultyRating(aimDifficultyValue);
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModTouchDevice))
|
||||||
|
aimRating = Math.Pow(aimRating, 0.8);
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModRelax))
|
||||||
|
aimRating *= 0.9;
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModMagnetised))
|
||||||
|
{
|
||||||
|
float magnetisedStrength = mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
|
||||||
|
aimRating *= 1.0 - magnetisedStrength;
|
||||||
|
}
|
||||||
|
|
||||||
|
double ratingMultiplier = 1.0;
|
||||||
|
|
||||||
|
double approachRateLengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||||
|
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||||
|
|
||||||
|
double approachRateFactor = 0.0;
|
||||||
|
if (approachRate > 10.33)
|
||||||
|
approachRateFactor = 0.3 * (approachRate - 10.33);
|
||||||
|
else if (approachRate < 8.0)
|
||||||
|
approachRateFactor = 0.05 * (8.0 - approachRate);
|
||||||
|
|
||||||
|
if (mods.Any(h => h is OsuModRelax))
|
||||||
|
approachRateFactor = 0.0;
|
||||||
|
|
||||||
|
ratingMultiplier += approachRateFactor * approachRateLengthBonus; // Buff for longer maps with high AR.
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModHidden))
|
||||||
|
{
|
||||||
|
double visibilityFactor = calculateAimVisibilityFactor(approachRate);
|
||||||
|
ratingMultiplier += CalculateVisibilityBonus(mods, approachRate, visibilityFactor, sliderFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// It is important to consider accuracy difficulty when scaling with accuracy.
|
||||||
|
ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
|
||||||
|
|
||||||
|
return aimRating * Math.Cbrt(ratingMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double ComputeSpeedRating(double speedDifficultyValue)
|
||||||
|
{
|
||||||
|
if (mods.Any(m => m is OsuModRelax))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
double speedRating = CalculateDifficultyRating(speedDifficultyValue);
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModAutopilot))
|
||||||
|
speedRating *= 0.5;
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModMagnetised))
|
||||||
|
{
|
||||||
|
// reduce speed rating because of the speed distance scaling, with maximum reduction being 0.7x
|
||||||
|
float magnetisedStrength = mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
|
||||||
|
speedRating *= 1.0 - magnetisedStrength * 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
double ratingMultiplier = 1.0;
|
||||||
|
|
||||||
|
double approachRateLengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||||
|
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||||
|
|
||||||
|
double approachRateFactor = 0.0;
|
||||||
|
if (approachRate > 10.33)
|
||||||
|
approachRateFactor = 0.3 * (approachRate - 10.33);
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModAutopilot))
|
||||||
|
approachRateFactor = 0.0;
|
||||||
|
|
||||||
|
ratingMultiplier += approachRateFactor * approachRateLengthBonus; // Buff for longer maps with high AR.
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModHidden))
|
||||||
|
{
|
||||||
|
double visibilityFactor = calculateSpeedVisibilityFactor(approachRate);
|
||||||
|
ratingMultiplier += CalculateVisibilityBonus(mods, approachRate, visibilityFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
ratingMultiplier *= 0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750;
|
||||||
|
|
||||||
|
return speedRating * Math.Cbrt(ratingMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double ComputeFlashlightRating(double flashlightDifficultyValue)
|
||||||
|
{
|
||||||
|
if (!mods.Any(m => m is OsuModFlashlight))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
double flashlightRating = CalculateDifficultyRating(flashlightDifficultyValue);
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModTouchDevice))
|
||||||
|
flashlightRating = Math.Pow(flashlightRating, 0.8);
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModRelax))
|
||||||
|
flashlightRating *= 0.7;
|
||||||
|
else if (mods.Any(m => m is OsuModAutopilot))
|
||||||
|
flashlightRating *= 0.4;
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModMagnetised))
|
||||||
|
{
|
||||||
|
float magnetisedStrength = mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
|
||||||
|
flashlightRating *= 1.0 - magnetisedStrength;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModDeflate))
|
||||||
|
{
|
||||||
|
float deflateInitialScale = mods.OfType<OsuModDeflate>().First().StartScale.Value;
|
||||||
|
flashlightRating *= Math.Clamp(DifficultyCalculationUtils.ReverseLerp(deflateInitialScale, 11, 1), 0.1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
double ratingMultiplier = 1.0;
|
||||||
|
|
||||||
|
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
||||||
|
ratingMultiplier *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
|
||||||
|
(totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0);
|
||||||
|
|
||||||
|
// It is important to consider accuracy difficulty when scaling with accuracy.
|
||||||
|
ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
|
||||||
|
|
||||||
|
return flashlightRating * Math.Sqrt(ratingMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculateAimVisibilityFactor(double approachRate)
|
||||||
|
{
|
||||||
|
const double ar_factor_end_point = 11.5;
|
||||||
|
|
||||||
|
double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10);
|
||||||
|
double arFactorStartingPoint = double.Lerp(9, 10.33, mechanicalDifficultyFactor);
|
||||||
|
|
||||||
|
return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculateSpeedVisibilityFactor(double approachRate)
|
||||||
|
{
|
||||||
|
const double ar_factor_end_point = 11.5;
|
||||||
|
|
||||||
|
double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10);
|
||||||
|
double arFactorStartingPoint = double.Lerp(10, 10.33, mechanicalDifficultyFactor);
|
||||||
|
|
||||||
|
return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates a visibility bonus that is applicable to Hidden and Traceable.
|
||||||
|
/// </summary>
|
||||||
|
public static double CalculateVisibilityBonus(Mod[] mods, double approachRate, double visibilityFactor = 1, double sliderFactor = 1)
|
||||||
|
{
|
||||||
|
// NOTE: TC's effect is only noticeable in performance calculations until lazer mods are accounted for server-side.
|
||||||
|
bool isAlwaysPartiallyVisible = mods.OfType<OsuModHidden>().Any(m => m.OnlyFadeApproachCircles.Value) || mods.OfType<OsuModTraceable>().Any();
|
||||||
|
|
||||||
|
// Start from normal curve, rewarding lower AR up to AR7
|
||||||
|
// TC forcefully requires a lower reading bonus for now as it's post-applied in PP which makes it multiplicative with the regular AR bonuses
|
||||||
|
// This means it has an advantage over HD, so we decrease the multiplier to compensate
|
||||||
|
// This should be removed once we're able to apply TC bonuses in SR (depends on real-time difficulty calculations being possible)
|
||||||
|
double readingBonus = (isAlwaysPartiallyVisible ? 0.025 : 0.04) * (12.0 - Math.Max(approachRate, 7));
|
||||||
|
|
||||||
|
readingBonus *= visibilityFactor;
|
||||||
|
|
||||||
|
// We want to reward slideraim on low AR less
|
||||||
|
double sliderVisibilityFactor = Math.Pow(sliderFactor, 3);
|
||||||
|
|
||||||
|
// For AR up to 0 - reduce reward for very low ARs when object is visible
|
||||||
|
if (approachRate < 7)
|
||||||
|
readingBonus += (isAlwaysPartiallyVisible ? 0.02 : 0.045) * (7.0 - Math.Max(approachRate, 0)) * sliderVisibilityFactor;
|
||||||
|
|
||||||
|
// Starting from AR0 - cap values so they won't grow to infinity
|
||||||
|
if (approachRate < 0)
|
||||||
|
readingBonus += (isAlwaysPartiallyVisible ? 0.01 : 0.1) * (1 - Math.Pow(1.5, approachRate)) * sliderVisibilityFactor;
|
||||||
|
|
||||||
|
return readingBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double CalculateDifficultyRating(double difficultyValue) => Math.Sqrt(difficultyValue) * difficulty_multiplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
@@ -36,22 +35,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly double AdjustedDeltaTime;
|
public readonly double AdjustedDeltaTime;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Amount of time elapsed between lastDifficultyObject's <see cref="DifficultyHitObject.EndTime"/> and <see cref="DifficultyHitObject.StartTime"/> capped to a minimum of <see cref="MIN_DELTA_TIME"/>ms.
|
|
||||||
/// </summary>
|
|
||||||
public double LastObjectEndDeltaTime { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Time (in ms) between the object first appearing and the time it needs to be clicked.
|
|
||||||
/// <see cref="OsuHitObject.TimePreempt"/> adjusted by clock rate.
|
|
||||||
/// </summary>
|
|
||||||
public readonly double Preempt;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normalised distance from the start position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
|
|
||||||
/// </summary>
|
|
||||||
public double JumpDistance { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Normalised distance from the "lazy" end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
|
/// Normalised distance from the "lazy" end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
|
||||||
/// <para>
|
/// <para>
|
||||||
@@ -118,29 +101,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
public double? Angle { get; private set; }
|
public double? Angle { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Angle of the vector created between current and current-1
|
/// Retrieves the full hit window for a Great <see cref="HitResult"/>.
|
||||||
/// normalised to consider symmetrical vectors in any axis to be the same angle.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double? NormalisedVectorAngle { get; private set; }
|
public double HitWindowGreat { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Selective bonus for maps with higher circle size.
|
/// Selective bonus for maps with higher circle size.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double SmallCircleBonus { get; private set; }
|
public double SmallCircleBonus { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Object's immediate OverallDifficulty value calculated from the raw hitwindow.
|
|
||||||
/// </summary>
|
|
||||||
public double OverallDifficulty
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
double hitWindowGreat = RawHitWindow(HitResult.Great) / ClockRate;
|
|
||||||
|
|
||||||
return (79.5 - hitWindowGreat) / 6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly OsuDifficultyHitObject? lastLastDifficultyObject;
|
private readonly OsuDifficultyHitObject? lastLastDifficultyObject;
|
||||||
private readonly OsuDifficultyHitObject? lastDifficultyObject;
|
private readonly OsuDifficultyHitObject? lastDifficultyObject;
|
||||||
|
|
||||||
@@ -152,11 +121,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
|
|
||||||
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
||||||
AdjustedDeltaTime = Math.Max(DeltaTime, MIN_DELTA_TIME);
|
AdjustedDeltaTime = Math.Max(DeltaTime, MIN_DELTA_TIME);
|
||||||
LastObjectEndDeltaTime = lastDifficultyObject != null ? Math.Max(StartTime - lastDifficultyObject.EndTime, MIN_DELTA_TIME) : AdjustedDeltaTime;
|
|
||||||
|
|
||||||
SmallCircleBonus = Math.Max(1.0, 1.0 + (30 - BaseObject.Radius) / 70);
|
SmallCircleBonus = Math.Max(1.0, 1.0 + (30 - BaseObject.Radius) / 40);
|
||||||
|
|
||||||
Preempt = BaseObject.TimePreempt / clockRate;
|
if (BaseObject is Slider sliderObject)
|
||||||
|
{
|
||||||
|
HitWindowGreat = 2 * sliderObject.HeadCircle.HitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HitWindowGreat = 2 * BaseObject.HitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||||
|
}
|
||||||
|
|
||||||
computeSliderCursorPosition();
|
computeSliderCursorPosition();
|
||||||
setDistances(clockRate);
|
setDistances(clockRate);
|
||||||
@@ -173,9 +148,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
}
|
}
|
||||||
|
|
||||||
double fadeInStartTime = BaseObject.StartTime - BaseObject.TimePreempt;
|
double fadeInStartTime = BaseObject.StartTime - BaseObject.TimePreempt;
|
||||||
|
double fadeInDuration = BaseObject.TimeFadeIn;
|
||||||
// Equal to `OsuHitObject.TimeFadeIn` minus any adjustments from the HD mod.
|
|
||||||
double fadeInDuration = 400 * Math.Min(1, BaseObject.TimePreempt / OsuHitObject.PREEMPT_MIN);
|
|
||||||
|
|
||||||
if (hidden)
|
if (hidden)
|
||||||
{
|
{
|
||||||
@@ -202,16 +175,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
{
|
{
|
||||||
double currDeltaTime = Math.Max(1, DeltaTime);
|
double currDeltaTime = Math.Max(1, DeltaTime);
|
||||||
double nextDeltaTime = Math.Max(1, osuNextObj.DeltaTime);
|
double nextDeltaTime = Math.Max(1, osuNextObj.DeltaTime);
|
||||||
|
|
||||||
double deltaDifference = Math.Abs(nextDeltaTime - currDeltaTime);
|
double deltaDifference = Math.Abs(nextDeltaTime - currDeltaTime);
|
||||||
|
|
||||||
double speedRatio = currDeltaTime / Math.Max(currDeltaTime, deltaDifference);
|
double speedRatio = currDeltaTime / Math.Max(currDeltaTime, deltaDifference);
|
||||||
double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / HitWindow(HitResult.Great)), 5);
|
double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / HitWindowGreat), 2);
|
||||||
|
return 1.0 - Math.Pow(speedRatio, 1 - windowRatio);
|
||||||
// Can't doubletap if circles don't intersect
|
|
||||||
double distanceFactor = Math.Pow(DifficultyCalculationUtils.ReverseLerp(LazyJumpDistance, NORMALISED_DIAMETER, NORMALISED_RADIUS), 2);
|
|
||||||
|
|
||||||
return 1.0 - Math.Pow(speedRatio, distanceFactor * (1 - windowRatio));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
@@ -222,12 +189,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
if (BaseObject is Slider currentSlider)
|
if (BaseObject is Slider currentSlider)
|
||||||
{
|
{
|
||||||
// Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
// Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
||||||
TravelDistance = LazyTravelDistance * Math.Max(1, Math.Pow(currentSlider.RepeatCount, 0.3));
|
TravelDistance = LazyTravelDistance * Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5);
|
||||||
TravelTime = Math.Max(LazyTravelTime / clockRate, MIN_DELTA_TIME);
|
TravelTime = Math.Max(LazyTravelTime / clockRate, MIN_DELTA_TIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
MinimumJumpTime = AdjustedDeltaTime;
|
|
||||||
|
|
||||||
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
|
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
|
||||||
if (BaseObject is Spinner || LastObject is Spinner)
|
if (BaseObject is Spinner || LastObject is Spinner)
|
||||||
return;
|
return;
|
||||||
@@ -237,8 +202,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
|
|
||||||
Vector2 lastCursorPosition = lastDifficultyObject != null ? getEndCursorPosition(lastDifficultyObject) : LastObject.StackedPosition;
|
Vector2 lastCursorPosition = lastDifficultyObject != null ? getEndCursorPosition(lastDifficultyObject) : LastObject.StackedPosition;
|
||||||
|
|
||||||
JumpDistance = (LastObject.StackedPosition - BaseObject.StackedPosition).Length * scalingFactor;
|
LazyJumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
|
||||||
LazyJumpDistance = (BaseObject.StackedPosition - lastCursorPosition).Length * scalingFactor;
|
MinimumJumpTime = AdjustedDeltaTime;
|
||||||
MinimumJumpDistance = LazyJumpDistance;
|
MinimumJumpDistance = LazyJumpDistance;
|
||||||
|
|
||||||
if (LastObject is Slider lastSlider && lastDifficultyObject != null)
|
if (LastObject is Slider lastSlider && lastDifficultyObject != null)
|
||||||
@@ -274,18 +239,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
|
|
||||||
if (lastLastDifficultyObject != null && lastLastDifficultyObject.BaseObject is not Spinner)
|
if (lastLastDifficultyObject != null && lastLastDifficultyObject.BaseObject is not Spinner)
|
||||||
{
|
{
|
||||||
if (lastDifficultyObject!.BaseObject is Slider prevSlider && lastDifficultyObject.TravelDistance > 0)
|
|
||||||
lastCursorPosition = prevSlider.HeadCircle.StackedPosition;
|
|
||||||
|
|
||||||
Vector2 lastLastCursorPosition = getEndCursorPosition(lastLastDifficultyObject);
|
Vector2 lastLastCursorPosition = getEndCursorPosition(lastLastDifficultyObject);
|
||||||
|
|
||||||
double angle = calculateAngle(BaseObject.StackedPosition, lastCursorPosition, lastLastCursorPosition);
|
Vector2 v1 = lastLastCursorPosition - LastObject.StackedPosition;
|
||||||
double sliderAngle = calculateSliderAngle(lastDifficultyObject!, lastLastCursorPosition);
|
Vector2 v2 = BaseObject.StackedPosition - lastCursorPosition;
|
||||||
|
|
||||||
Vector2 v = BaseObject.StackedPosition - lastCursorPosition;
|
float dot = Vector2.Dot(v1, v2);
|
||||||
NormalisedVectorAngle = Math.Atan2(Math.Abs(v.Y), Math.Abs(v.X));
|
float det = v1.X * v2.Y - v1.Y * v2.X;
|
||||||
|
|
||||||
Angle = Math.Min(angle, sliderAngle);
|
Angle = Math.Abs(Math.Atan2(det, dot));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,30 +359,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private double calculateSliderAngle(OsuDifficultyHitObject lastDifficultyObject, Vector2 lastLastCursorPosition)
|
|
||||||
{
|
|
||||||
Vector2 lastCursorPosition = getEndCursorPosition(lastDifficultyObject);
|
|
||||||
|
|
||||||
if (lastDifficultyObject.BaseObject is Slider prevSlider && lastDifficultyObject.TravelDistance > 0)
|
|
||||||
{
|
|
||||||
OsuHitObject secondLastNestedObject = (OsuHitObject)prevSlider.NestedHitObjects[^2];
|
|
||||||
lastLastCursorPosition = secondLastNestedObject.StackedPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
return calculateAngle(BaseObject.StackedPosition, lastCursorPosition, lastLastCursorPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double calculateAngle(Vector2 currentPosition, Vector2 lastPosition, Vector2 lastLastPosition)
|
|
||||||
{
|
|
||||||
Vector2 v1 = lastLastPosition - lastPosition;
|
|
||||||
Vector2 v2 = currentPosition - lastPosition;
|
|
||||||
|
|
||||||
float dot = Vector2.Dot(v1, v2);
|
|
||||||
float det = v1.X * v2.Y - v1.Y * v2.X;
|
|
||||||
|
|
||||||
return Math.Abs(Math.Atan2(det, dot));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Vector2 getEndCursorPosition(OsuDifficultyHitObject difficultyHitObject)
|
private Vector2 getEndCursorPosition(OsuDifficultyHitObject difficultyHitObject)
|
||||||
{
|
{
|
||||||
return difficultyHitObject.LazyEndPosition ?? difficultyHitObject.BaseObject.StackedPosition;
|
return difficultyHitObject.LazyEndPosition ?? difficultyHitObject.BaseObject.StackedPosition;
|
||||||
|
|||||||
@@ -4,14 +4,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators.Aim;
|
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Utils;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||||
@@ -19,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
/// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Aim : VariableLengthStrainSkill
|
public class Aim : OsuStrainSkill
|
||||||
{
|
{
|
||||||
public readonly bool IncludeSliders;
|
public readonly bool IncludeSliders;
|
||||||
|
|
||||||
@@ -31,39 +27,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
|
|
||||||
private double currentStrain;
|
private double currentStrain;
|
||||||
|
|
||||||
private double skillMultiplierSnap => 70.9;
|
private double skillMultiplier => 26;
|
||||||
private double skillMultiplierAgility => 2.35;
|
private double strainDecayBase => 0.15;
|
||||||
private double skillMultiplierFlow => 242.0;
|
|
||||||
private double skillMultiplierTotal => 1.12;
|
|
||||||
private double combinedSnapNormExponent => 1.2;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
|
|
||||||
/// This is done in order to decrease their impact on the overall difficulty of the map for this skill.
|
|
||||||
/// </summary>
|
|
||||||
private int reducedSectionTime => 4000;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The baseline multiplier applied to the section with the biggest strain.
|
|
||||||
/// </summary>
|
|
||||||
private double reducedStrainBaseline => 0.727;
|
|
||||||
|
|
||||||
private readonly List<double> sliderStrains = new List<double>();
|
private readonly List<double> sliderStrains = new List<double>();
|
||||||
|
|
||||||
private double strainDecay(double ms) => Math.Pow(0.2, ms / 1000);
|
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||||
|
|
||||||
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) =>
|
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime);
|
||||||
currentStrain * strainDecay(time - current.Previous(0).StartTime);
|
|
||||||
|
|
||||||
protected override double StrainValueAt(DifficultyHitObject current)
|
protected override double StrainValueAt(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
if (Mods.Any(m => m is OsuModAutopilot))
|
currentStrain *= strainDecay(current.DeltaTime);
|
||||||
return 0;
|
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplier;
|
||||||
|
|
||||||
double decay = strainDecay(((OsuDifficultyHitObject)current).AdjustedDeltaTime);
|
|
||||||
|
|
||||||
currentStrain *= decay;
|
|
||||||
currentStrain += calculateAdjustedDifficulty(current) * (1 - decay);
|
|
||||||
|
|
||||||
if (current.BaseObject is Slider)
|
if (current.BaseObject is Slider)
|
||||||
sliderStrains.Add(currentStrain);
|
sliderStrains.Add(currentStrain);
|
||||||
@@ -71,75 +47,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
return currentStrain;
|
return currentStrain;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double calculateAdjustedDifficulty(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
double snapDifficulty = SnapAimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplierSnap;
|
|
||||||
double agilityDifficulty = AgilityEvaluator.EvaluateDifficultyOf(current) * skillMultiplierAgility;
|
|
||||||
double flowDifficulty = FlowAimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplierFlow;
|
|
||||||
|
|
||||||
double totalDifficulty = calculateTotalValue(snapDifficulty, agilityDifficulty, flowDifficulty);
|
|
||||||
|
|
||||||
if (Mods.Any(m => m is OsuModMagnetised))
|
|
||||||
{
|
|
||||||
float magnetisedStrength = Mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
|
|
||||||
totalDifficulty *= 1.0 - magnetisedStrength;
|
|
||||||
}
|
|
||||||
|
|
||||||
totalDifficulty *= 0.985 + Math.Pow(Math.Max(0, ((OsuDifficultyHitObject)current).OverallDifficulty), 2) / 4000;
|
|
||||||
|
|
||||||
return totalDifficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double calculateTotalValue(double snapDifficulty, double agilityDifficulty, double flowDifficulty)
|
|
||||||
{
|
|
||||||
// We compare flow to combined snap and agility because snap by itself doesn't have enough difficulty to be above flow on streams
|
|
||||||
// Agility on the other hand is supposed to measure the rate of cursor velocity changes while snapping
|
|
||||||
// So snapping every circle on a stream requires an enormous amount of agility at which point it's easier to flow
|
|
||||||
double combinedSnapDifficulty = DifficultyCalculationUtils.Norm(combinedSnapNormExponent, snapDifficulty, agilityDifficulty);
|
|
||||||
|
|
||||||
double pSnap = calculateSnapFlowProbability(flowDifficulty / combinedSnapDifficulty);
|
|
||||||
double pFlow = 1 - pSnap;
|
|
||||||
|
|
||||||
if (Mods.Any(m => m is OsuModTouchDevice))
|
|
||||||
{
|
|
||||||
// we don't adjust agility here since agility represents TD difficulty in a decent enough way
|
|
||||||
snapDifficulty = Math.Pow(snapDifficulty, 0.89);
|
|
||||||
combinedSnapDifficulty = DifficultyCalculationUtils.Norm(combinedSnapNormExponent, snapDifficulty, agilityDifficulty);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Mods.Any(m => m is OsuModRelax))
|
|
||||||
{
|
|
||||||
combinedSnapDifficulty *= 0.75;
|
|
||||||
flowDifficulty *= 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
double totalDifficulty = combinedSnapDifficulty * pSnap + flowDifficulty * pFlow;
|
|
||||||
|
|
||||||
double totalStrain = totalDifficulty * skillMultiplierTotal;
|
|
||||||
|
|
||||||
return totalStrain;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A function that turns the ratio of snap : flow into the probability of snapping/flowing
|
|
||||||
// It has the constraints:
|
|
||||||
// P(snap) + P(flow) = 1 (the object is always either snapped or flowed)
|
|
||||||
// P(snap) = f(snap/flow), P(flow) = f(flow/snap) (ie snap and flow are symmetric and reversible)
|
|
||||||
// Therefore: f(x) + f(1/x) = 1
|
|
||||||
// 0 <= f(x) <= 1 (cannot have negative or greater than 100% probability of snapping or flowing)
|
|
||||||
// This logistic function is a solution, which fits nicely with the general idea of interpolation and provides a tuneable constant
|
|
||||||
private static double calculateSnapFlowProbability(double ratio)
|
|
||||||
{
|
|
||||||
const double k = 7.27;
|
|
||||||
|
|
||||||
if (ratio == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
if (double.IsNaN(ratio))
|
|
||||||
return 1;
|
|
||||||
|
|
||||||
return DifficultyCalculationUtils.Logistic(-k * Math.Log(ratio));
|
|
||||||
}
|
|
||||||
|
|
||||||
public double GetDifficultSliders()
|
public double GetDifficultSliders()
|
||||||
{
|
{
|
||||||
if (sliderStrains.Count == 0)
|
if (sliderStrains.Count == 0)
|
||||||
@@ -153,97 +60,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
return sliderStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0))));
|
return sliderStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0))));
|
||||||
}
|
}
|
||||||
|
|
||||||
public double CountTopWeightedSliders(double difficultyValue)
|
public double CountTopWeightedSliders() => OsuStrainUtils.CountTopWeightedSliders(sliderStrains, DifficultyValue());
|
||||||
{
|
|
||||||
if (sliderStrains.Count == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
double consistentTopStrain = difficultyValue * (1 - DecayWeight); // What would the top strain be if all strain values were identical
|
|
||||||
|
|
||||||
if (consistentTopStrain == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
// Use a weighted sum of all strains. Constants are arbitrary and give nice values
|
|
||||||
return sliderStrains.Sum(s => DifficultyCalculationUtils.Logistic(s / consistentTopStrain, 0.88, 10, 1.1));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override double DifficultyValue()
|
|
||||||
{
|
|
||||||
double difficulty = 0;
|
|
||||||
double time = 0;
|
|
||||||
|
|
||||||
var strains = getReducedStrainPeaks();
|
|
||||||
|
|
||||||
// Difficulty is a continuous weighted sum of the sorted strains
|
|
||||||
foreach (StrainPeak strain in strains)
|
|
||||||
{
|
|
||||||
/* Weighting function can be thought of as:
|
|
||||||
b
|
|
||||||
∫ DecayWeight^x dx
|
|
||||||
a
|
|
||||||
where a = startTime and b = endTime
|
|
||||||
|
|
||||||
Technically, the function below has been slightly modified from the equation above.
|
|
||||||
The real function would be
|
|
||||||
double weight = Math.Pow(DecayWeight, startTime) - Math.Pow(DecayWeight, endTime);
|
|
||||||
...
|
|
||||||
return difficulty / Math.Log(1 / DecayWeight);
|
|
||||||
E.g. for a DecayWeight of 0.9, we're multiplying by 10 instead of 9.49122...
|
|
||||||
|
|
||||||
This change makes it so that a map composed solely of MaxSectionLength chunks will have the exact same value when summed in this class and StrainSkill.
|
|
||||||
Doing this ensures the relationship between strain values and difficulty values remains the same between the two classes.
|
|
||||||
*/
|
|
||||||
double startTime = time;
|
|
||||||
double endTime = time + strain.SectionLength / MaxSectionLength;
|
|
||||||
|
|
||||||
double weight = Math.Pow(DecayWeight, startTime) - Math.Pow(DecayWeight, endTime);
|
|
||||||
|
|
||||||
difficulty += strain.Value * weight;
|
|
||||||
time = endTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
return difficulty / (1 - DecayWeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a sorted enumerable of strain peaks with the highest values reduced.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
private IEnumerable<StrainPeak> getReducedStrainPeaks()
|
|
||||||
{
|
|
||||||
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
|
||||||
// These sections will not contribute to the difficulty.
|
|
||||||
var peaks = GetCurrentStrainPeaks().Where(p => p.Value > 0);
|
|
||||||
|
|
||||||
List<StrainPeak> strains = peaks.OrderByDescending(p => p.Value).ToList();
|
|
||||||
|
|
||||||
const int chunk_size = 20;
|
|
||||||
double time = 0;
|
|
||||||
int strainsToRemove = 0; // All strains are removed at the end for optimization purposes
|
|
||||||
|
|
||||||
// We are reducing the highest strains first to account for extreme difficulty spikes
|
|
||||||
// Strains are split into 20ms chunks to try to mitigate inconsistencies caused by reducing strains
|
|
||||||
while (strains.Count > strainsToRemove && time < reducedSectionTime)
|
|
||||||
{
|
|
||||||
StrainPeak strain = strains[strainsToRemove];
|
|
||||||
|
|
||||||
for (double addedTime = 0; addedTime < strain.SectionLength; addedTime += chunk_size)
|
|
||||||
{
|
|
||||||
double scale = Math.Log10(Interpolation.Lerp(1, 10, Math.Clamp((time + addedTime) / reducedSectionTime, 0, 1)));
|
|
||||||
|
|
||||||
strains.Add(new StrainPeak(
|
|
||||||
strain.Value * Interpolation.Lerp(reducedStrainBaseline, 1.0, scale),
|
|
||||||
Math.Min(chunk_size, strain.SectionLength - addedTime)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
time += strain.SectionLength;
|
|
||||||
strainsToRemove++;
|
|
||||||
}
|
|
||||||
|
|
||||||
strains.RemoveRange(0, strainsToRemove);
|
|
||||||
|
|
||||||
return strains.OrderByDescending(p => p.Value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||||
@@ -18,15 +16,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Flashlight : StrainSkill
|
public class Flashlight : StrainSkill
|
||||||
{
|
{
|
||||||
private readonly int totalObjects;
|
private readonly bool hasHiddenMod;
|
||||||
|
|
||||||
public Flashlight(Mod[] mods, int totalObjects)
|
public Flashlight(Mod[] mods)
|
||||||
: base(mods)
|
: base(mods)
|
||||||
{
|
{
|
||||||
this.totalObjects = totalObjects;
|
hasHiddenMod = mods.Any(m => m is OsuModHidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double skillMultiplier => 0.058;
|
private double skillMultiplier => 0.05512;
|
||||||
private double strainDecayBase => 0.15;
|
private double strainDecayBase => 0.15;
|
||||||
|
|
||||||
private double currentStrain;
|
private double currentStrain;
|
||||||
@@ -37,55 +35,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
|
|
||||||
protected override double StrainValueAt(DifficultyHitObject current)
|
protected override double StrainValueAt(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
if (!Mods.Any(m => m is OsuModFlashlight))
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
currentStrain *= strainDecay(current.DeltaTime);
|
currentStrain *= strainDecay(current.DeltaTime);
|
||||||
currentStrain += calculateAdjustedDifficulty(current) * skillMultiplier;
|
currentStrain += FlashlightEvaluator.EvaluateDifficultyOf(current, hasHiddenMod) * skillMultiplier;
|
||||||
|
|
||||||
return currentStrain;
|
return currentStrain;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double calculateAdjustedDifficulty(DifficultyHitObject current)
|
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum();
|
||||||
{
|
|
||||||
double difficulty = FlashlightEvaluator.EvaluateDifficultyOf(current, Mods);
|
|
||||||
|
|
||||||
if (Mods.Any(m => m is OsuModTouchDevice))
|
|
||||||
difficulty = Math.Pow(difficulty, 0.9);
|
|
||||||
|
|
||||||
if (Mods.Any(m => m is OsuModMagnetised))
|
|
||||||
{
|
|
||||||
float magnetisedStrength = Mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
|
|
||||||
difficulty *= 1.0 - magnetisedStrength;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Mods.Any(m => m is OsuModDeflate))
|
|
||||||
{
|
|
||||||
float deflateInitialScale = Mods.OfType<OsuModDeflate>().First().StartScale.Value;
|
|
||||||
difficulty *= Math.Clamp(DifficultyCalculationUtils.ReverseLerp(deflateInitialScale, 11, 1), 0.1, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Mods.Any(m => m is OsuModRelax))
|
|
||||||
difficulty *= 0.7;
|
|
||||||
|
|
||||||
if (Mods.Any(m => m is OsuModAutopilot))
|
|
||||||
difficulty *= 0.4;
|
|
||||||
|
|
||||||
difficulty *= 0.985 + Math.Pow(Math.Max(0, ((OsuDifficultyHitObject)current).OverallDifficulty), 2) / 4000;
|
|
||||||
|
|
||||||
return difficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override double DifficultyValue()
|
|
||||||
{
|
|
||||||
double sum = GetCurrentStrainPeaks().Sum();
|
|
||||||
|
|
||||||
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
|
||||||
sum *= 0.7 + 0.1 * Math.Min(1.0, totalObjects / 200.0) +
|
|
||||||
(totalObjects > 200 ? 0.2 * Math.Min(1.0, (totalObjects - 200) / 200.0) : 0.0);
|
|
||||||
|
|
||||||
return sum;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static double DifficultyToPerformance(double difficulty) => 25 * Math.Pow(difficulty, 2);
|
public static double DifficultyToPerformance(double difficulty) => 25 * Math.Pow(difficulty, 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// 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 osu.Game.Rulesets.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||||
|
{
|
||||||
|
public abstract class OsuStrainSkill : StrainSkill
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
|
||||||
|
/// This is done in order to decrease their impact on the overall difficulty of the map for this skill.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual int ReducedSectionCount => 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The baseline multiplier applied to the section with the biggest strain.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual double ReducedStrainBaseline => 0.75;
|
||||||
|
|
||||||
|
protected OsuStrainSkill(Mod[] mods)
|
||||||
|
: base(mods)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override double DifficultyValue()
|
||||||
|
{
|
||||||
|
double difficulty = 0;
|
||||||
|
double weight = 1;
|
||||||
|
|
||||||
|
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
||||||
|
// These sections will not contribute to the difficulty.
|
||||||
|
var peaks = GetCurrentStrainPeaks().Where(p => p > 0);
|
||||||
|
|
||||||
|
List<double> strains = peaks.OrderDescending().ToList();
|
||||||
|
|
||||||
|
// We are reducing the highest strains first to account for extreme difficulty spikes
|
||||||
|
for (int i = 0; i < Math.Min(strains.Count, ReducedSectionCount); i++)
|
||||||
|
{
|
||||||
|
double scale = Math.Log10(Interpolation.Lerp(1, 10, Math.Clamp((float)i / ReducedSectionCount, 0, 1)));
|
||||||
|
strains[i] *= Interpolation.Lerp(ReducedStrainBaseline, 1.0, scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Difficulty is the weighted sum of the highest strains from every section.
|
||||||
|
// We're sorting from highest to lowest strain.
|
||||||
|
foreach (double strain in strains.OrderDescending())
|
||||||
|
{
|
||||||
|
difficulty += strain * weight;
|
||||||
|
weight *= DecayWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return difficulty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,124 +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 osu.Framework.Utils;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|
||||||
{
|
|
||||||
public class Reading : HarmonicSkill
|
|
||||||
{
|
|
||||||
private readonly List<DifficultyHitObject> objectList = new List<DifficultyHitObject>();
|
|
||||||
|
|
||||||
private readonly bool hasHiddenMod;
|
|
||||||
|
|
||||||
public Reading(Mod[] mods)
|
|
||||||
: base(mods)
|
|
||||||
{
|
|
||||||
hasHiddenMod = mods.OfType<OsuModHidden>().Any(m => !m.OnlyFadeApproachCircles.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double currentStrain;
|
|
||||||
|
|
||||||
private double skillMultiplier => 2.5;
|
|
||||||
private double strainDecayBase => 0.8;
|
|
||||||
|
|
||||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
|
||||||
|
|
||||||
protected override double ObjectDifficultyOf(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
objectList.Add(current);
|
|
||||||
|
|
||||||
double decay = strainDecay(current.DeltaTime);
|
|
||||||
|
|
||||||
currentStrain *= decay;
|
|
||||||
currentStrain += calculateAdjustedDifficulty(current) * (1 - decay) * skillMultiplier;
|
|
||||||
|
|
||||||
return currentStrain;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double calculateAdjustedDifficulty(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
double difficulty = ReadingEvaluator.EvaluateDifficultyOf(current, hasHiddenMod);
|
|
||||||
|
|
||||||
if (Mods.Any(m => m is OsuModTouchDevice))
|
|
||||||
difficulty = Math.Pow(difficulty, 0.89);
|
|
||||||
|
|
||||||
if (Mods.Any(m => m is OsuModMagnetised))
|
|
||||||
{
|
|
||||||
float magnetisedStrength = Mods.OfType<OsuModMagnetised>().First().AttractionStrength.Value;
|
|
||||||
difficulty *= 1.0 - magnetisedStrength;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Mods.Any(m => m is OsuModRelax))
|
|
||||||
difficulty *= 0.4;
|
|
||||||
|
|
||||||
if (Mods.Any(m => m is OsuModAutopilot))
|
|
||||||
difficulty *= 0.1;
|
|
||||||
|
|
||||||
difficulty *= 0.825 + Math.Pow(Math.Max(0, ((OsuDifficultyHitObject)current).OverallDifficulty), 2.2) / 1125.0;
|
|
||||||
|
|
||||||
return difficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void ApplyDifficultyTransformation(double[] difficulties)
|
|
||||||
{
|
|
||||||
const double reduced_difficulty_base_line = 0.0; // Assume the first seconds are completely memorised
|
|
||||||
|
|
||||||
int reducedNoteCount = calculateReducedNoteCount();
|
|
||||||
|
|
||||||
for (int i = 0; i < Math.Min(difficulties.Length, reducedNoteCount); i++)
|
|
||||||
{
|
|
||||||
double scale = Math.Log10(Interpolation.Lerp(1, 10, Math.Clamp((double)i / reducedNoteCount, 0, 1)));
|
|
||||||
difficulties[i] *= Interpolation.Lerp(reduced_difficulty_base_line, 1.0, scale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int calculateReducedNoteCount()
|
|
||||||
{
|
|
||||||
const double reduced_difficulty_duration = 60 * 1000;
|
|
||||||
|
|
||||||
if (objectList.Count == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
double reducedDuration = objectList.First().StartTime + reduced_difficulty_duration;
|
|
||||||
|
|
||||||
int reducedNoteCount = 0;
|
|
||||||
|
|
||||||
foreach (var hitObject in objectList)
|
|
||||||
{
|
|
||||||
if (hitObject.StartTime > reducedDuration)
|
|
||||||
break;
|
|
||||||
|
|
||||||
reducedNoteCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return reducedNoteCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override double CountTopWeightedObjectDifficulties(double difficultyValue)
|
|
||||||
{
|
|
||||||
if (ObjectDifficulties.Count == 0)
|
|
||||||
return 0.0;
|
|
||||||
|
|
||||||
if (NoteWeightSum == 0)
|
|
||||||
return 0.0;
|
|
||||||
|
|
||||||
double consistentTopNote = difficultyValue / NoteWeightSum; // What would the top difficulty be if all object difficulties were identical
|
|
||||||
|
|
||||||
if (consistentTopNote == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
return ObjectDifficulties.Sum(d => DifficultyCalculationUtils.Logistic(d / consistentTopNote, 1.15, 5, 1.1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,33 +3,30 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators.Speed;
|
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Game.Rulesets.Osu.Difficulty.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit.
|
/// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Speed : HarmonicSkill
|
public class Speed : OsuStrainSkill
|
||||||
{
|
{
|
||||||
private double skillMultiplier => 1.16;
|
private double skillMultiplier => 1.47;
|
||||||
|
private double strainDecayBase => 0.3;
|
||||||
|
|
||||||
|
private double currentStrain;
|
||||||
|
private double currentRhythm;
|
||||||
|
|
||||||
private readonly List<double> sliderStrains = new List<double>();
|
private readonly List<double> sliderStrains = new List<double>();
|
||||||
|
|
||||||
private double currentStrain;
|
protected override int ReducedSectionCount => 5;
|
||||||
|
|
||||||
private double strainDecayBase => 0.3;
|
|
||||||
|
|
||||||
protected override double HarmonicScale => 20;
|
|
||||||
protected override double DecayExponent => 0.9;
|
|
||||||
|
|
||||||
public Speed(Mod[] mods)
|
public Speed(Mod[] mods)
|
||||||
: base(mods)
|
: base(mods)
|
||||||
@@ -38,17 +35,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
|
|
||||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||||
|
|
||||||
protected override double ObjectDifficultyOf(DifficultyHitObject current)
|
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => (currentStrain * currentRhythm) * strainDecay(time - current.Previous(0).StartTime);
|
||||||
|
|
||||||
|
protected override double StrainValueAt(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
if (Mods.Any(m => m is OsuModRelax))
|
currentStrain *= strainDecay(((OsuDifficultyHitObject)current).AdjustedDeltaTime);
|
||||||
return 0;
|
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, Mods) * skillMultiplier;
|
||||||
|
|
||||||
double decay = strainDecay(((OsuDifficultyHitObject)current).AdjustedDeltaTime);
|
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
|
||||||
|
|
||||||
currentStrain *= decay;
|
|
||||||
currentStrain += calculateAdjustedDifficulty(current) * (1 - decay) * skillMultiplier;
|
|
||||||
|
|
||||||
double currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
|
|
||||||
|
|
||||||
double totalStrain = currentStrain * currentRhythm;
|
double totalStrain = currentStrain * currentRhythm;
|
||||||
|
|
||||||
@@ -58,44 +52,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
return totalStrain;
|
return totalStrain;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double calculateAdjustedDifficulty(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
double difficulty = SpeedEvaluator.EvaluateDifficultyOf(current);
|
|
||||||
|
|
||||||
if (Mods.Any(m => m is OsuModAutopilot))
|
|
||||||
difficulty *= 0.5;
|
|
||||||
|
|
||||||
return difficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double RelevantNoteCount()
|
public double RelevantNoteCount()
|
||||||
{
|
{
|
||||||
if (ObjectDifficulties.Count == 0)
|
if (ObjectStrains.Count == 0)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
double maxStrain = ObjectDifficulties.Max();
|
double maxStrain = ObjectStrains.Max();
|
||||||
|
|
||||||
if (maxStrain == 0)
|
if (maxStrain == 0)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return ObjectDifficulties.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0))));
|
return ObjectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0))));
|
||||||
}
|
}
|
||||||
|
|
||||||
public double CountTopWeightedSliders(double difficultyValue)
|
public double CountTopWeightedSliders() => OsuStrainUtils.CountTopWeightedSliders(sliderStrains, DifficultyValue());
|
||||||
{
|
|
||||||
if (sliderStrains.Count == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
if (NoteWeightSum == 0)
|
|
||||||
return 0.0;
|
|
||||||
|
|
||||||
double consistentTopNote = difficultyValue / NoteWeightSum; // What would the top note be if all note values were identical
|
|
||||||
|
|
||||||
if (consistentTopNote == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
// Use a weighted sum of all notes. Constants are arbitrary and give nice values
|
|
||||||
return sliderStrains.Sum(s => DifficultyCalculationUtils.Logistic(s / consistentTopNote, 0.88, 10, 1.1));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Utils;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Difficulty.Utils
|
||||||
|
{
|
||||||
|
public static class OsuStrainUtils
|
||||||
|
{
|
||||||
|
public static double CountTopWeightedSliders(IReadOnlyCollection<double> sliderStrains, double difficultyValue)
|
||||||
|
{
|
||||||
|
if (sliderStrains.Count == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
double consistentTopStrain = difficultyValue / 10; // What would the top strain be if all strain values were identical
|
||||||
|
|
||||||
|
if (consistentTopStrain == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Use a weighted sum of all strains. Constants are arbitrary and give nice values
|
||||||
|
return sliderStrains.Sum(s => DifficultyCalculationUtils.Logistic(s / consistentTopStrain, 0.88, 10, 1.1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private EditorClock? editorClock { get; set; }
|
private EditorClock? editorClock { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuSliderVelocityToolboxGroup? sliderVelocityToolbox { get; set; }
|
||||||
|
|
||||||
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
|
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
|
||||||
|
|
||||||
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
||||||
@@ -111,9 +114,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
|
||||||
|
|
||||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||||
{
|
{
|
||||||
var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime);
|
var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime);
|
||||||
@@ -129,11 +129,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
case SliderPlacementState.Initial:
|
case SliderPlacementState.Initial:
|
||||||
BeginPlacement();
|
BeginPlacement();
|
||||||
|
|
||||||
double? nearestSliderVelocity = (editorBeatmap
|
HitObject.SliderVelocityMultiplier = sliderVelocityToolbox?.SliderVelocity.Value ?? 1;
|
||||||
.HitObjects
|
|
||||||
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocityMultiplier;
|
|
||||||
|
|
||||||
HitObject.SliderVelocityMultiplier = nearestSliderVelocity ?? 1;
|
|
||||||
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
|
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
|
||||||
|
|
||||||
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.
|
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
public partial class FreehandSliderToolboxGroup : EditorToolboxGroup
|
public partial class FreehandSliderToolboxGroup : EditorToolboxGroup
|
||||||
{
|
{
|
||||||
public FreehandSliderToolboxGroup()
|
public FreehandSliderToolboxGroup()
|
||||||
: base("slider")
|
: base("freehand")
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,13 @@ using osu.Game.Graphics;
|
|||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Edit.Tools;
|
using osu.Game.Rulesets.Edit.Tools;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
{
|
{
|
||||||
public class HitCircleCompositionTool : CompositionTool
|
public class HitCircleCompositionTool : CompositionTool
|
||||||
{
|
{
|
||||||
public HitCircleCompositionTool()
|
public HitCircleCompositionTool()
|
||||||
: base(nameof(HitCircle))
|
: base("Hit circle")
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
[Cached(typeof(IDistanceSnapProvider))]
|
[Cached(typeof(IDistanceSnapProvider))]
|
||||||
public readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
|
public readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private readonly OsuSliderVelocityToolboxGroup sliderVelocityToolboxGroup = new OsuSliderVelocityToolboxGroup();
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
protected readonly OsuGridToolboxGroup OsuGridToolboxGroup = new OsuGridToolboxGroup();
|
protected readonly OsuGridToolboxGroup OsuGridToolboxGroup = new OsuGridToolboxGroup();
|
||||||
|
|
||||||
@@ -111,6 +114,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
RightToolbox.AddRange(new Drawable[]
|
RightToolbox.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
|
sliderVelocityToolboxGroup,
|
||||||
OsuGridToolboxGroup,
|
OsuGridToolboxGroup,
|
||||||
new TransformToolboxGroup
|
new TransformToolboxGroup
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
// 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.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Caching;
|
||||||
|
using osu.Framework.Extensions.LocalisationExtensions;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
|
{
|
||||||
|
public partial class OsuSliderVelocityToolboxGroup : EditorToolboxGroup
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the last slider's velocity should be used (if available).
|
||||||
|
/// </summary>
|
||||||
|
private bool useLastSliderVelocity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The slider velocity to be used for new object placements.
|
||||||
|
/// </summary>
|
||||||
|
public IBindable<double> SliderVelocity => sliderVelocity;
|
||||||
|
|
||||||
|
private readonly BindableDouble sliderVelocity = new BindableDouble(1)
|
||||||
|
{
|
||||||
|
Precision = 0.01,
|
||||||
|
MinValue = 0.1,
|
||||||
|
MaxValue = 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
private ExpandableSlider<double> slider = null!;
|
||||||
|
private ExpandableButton useLastSliderButton = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private EditorClock editorClock { get; set; } = null!;
|
||||||
|
|
||||||
|
private bool syncingBindables;
|
||||||
|
private double lastClockPosition = double.NegativeInfinity;
|
||||||
|
private readonly Cached<Slider?> sliderVelocitySourceObject = new Cached<Slider?>();
|
||||||
|
|
||||||
|
public OsuSliderVelocityToolboxGroup()
|
||||||
|
: base("velocity")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Spacing = new Vector2(5);
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
slider = new ExpandableSlider<double>
|
||||||
|
{
|
||||||
|
ExpandedLabelText = "Slider velocity",
|
||||||
|
Current = new BindableDouble(1)
|
||||||
|
{
|
||||||
|
Precision = 0.01,
|
||||||
|
MinValue = 0.1,
|
||||||
|
MaxValue = 10,
|
||||||
|
},
|
||||||
|
KeyboardStep = 0.1f,
|
||||||
|
},
|
||||||
|
useLastSliderButton = new ExpandableButton
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Action = () =>
|
||||||
|
{
|
||||||
|
useLastSliderVelocity = true;
|
||||||
|
sliderVelocitySourceObject.Invalidate();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
// set unconditionally to true initially.
|
||||||
|
// if there is no object available to get the slider velocity from, the code in `Update()` will handle that.
|
||||||
|
useLastSliderVelocity = true;
|
||||||
|
|
||||||
|
sliderVelocity.BindValueChanged(_ => updateSliderFromVelocity(), true);
|
||||||
|
slider.Current.BindValueChanged(_ =>
|
||||||
|
{
|
||||||
|
updateVelocityFromSlider();
|
||||||
|
updateContractedText();
|
||||||
|
});
|
||||||
|
updateContractedText();
|
||||||
|
useLastSliderButton.Expanded.BindValueChanged(_ => sliderVelocitySourceObject.Invalidate());
|
||||||
|
|
||||||
|
editorBeatmap.HitObjectAdded += invalidateSliderVelocitySourceObject;
|
||||||
|
editorBeatmap.HitObjectUpdated += invalidateSliderVelocitySourceObject;
|
||||||
|
editorBeatmap.HitObjectRemoved += invalidateSliderVelocitySourceObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateContractedText()
|
||||||
|
{
|
||||||
|
slider.ContractedLabelText = LocalisableString.Interpolate($@"SV: {slider.Current.Value.ToLocalisableString("N2")}x");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the displayed value of this toolbox's slider from a change to <see cref="SliderVelocity"/>
|
||||||
|
/// (which is the source-of-truth used for new object placements).
|
||||||
|
/// This is only relevant when <see cref="useLastSliderVelocity"/> is true,
|
||||||
|
/// in which case this code is responsible for propagating the velocity from <see cref="sliderVelocitySourceObject"/> to the slider.
|
||||||
|
/// </summary>
|
||||||
|
private void updateSliderFromVelocity()
|
||||||
|
{
|
||||||
|
if (syncingBindables)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!useLastSliderVelocity)
|
||||||
|
return;
|
||||||
|
|
||||||
|
syncingBindables = true;
|
||||||
|
slider.Current.Value = sliderVelocity.Value;
|
||||||
|
syncingBindables = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the value of <see cref="SliderVelocity"/> from a change to the slider's state.
|
||||||
|
/// This change is assumed to be user-provoked, and therefore <see cref="useLastSliderVelocity"/> is switched unconditionally off
|
||||||
|
/// as the presumed intent is to override the velocity from <see cref="sliderVelocitySourceObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
private void updateVelocityFromSlider()
|
||||||
|
{
|
||||||
|
if (syncingBindables)
|
||||||
|
return;
|
||||||
|
|
||||||
|
syncingBindables = true;
|
||||||
|
useLastSliderVelocity = false;
|
||||||
|
sliderVelocity.Value = slider.Current.Value;
|
||||||
|
syncingBindables = false;
|
||||||
|
sliderVelocitySourceObject.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invalidateSliderVelocitySourceObject(HitObject _) => sliderVelocitySourceObject.Invalidate();
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
if (editorClock.CurrentTime != lastClockPosition)
|
||||||
|
{
|
||||||
|
sliderVelocitySourceObject.Invalidate();
|
||||||
|
lastClockPosition = editorClock.CurrentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Three possible causes of invalidation:
|
||||||
|
// - The user seeked the clock, which means a different velocity source object needs to be used.
|
||||||
|
// - Some change to the beatmap was made, which means the previously-used velocity source object may no longer be the most relevant one.
|
||||||
|
// - The user is interacting with the toolbox in a way that requires a visual state update
|
||||||
|
// (hovered to expand it, clicked the button to use last slider's velocity, or dragged the manual velocity slider).
|
||||||
|
// This is a procedural one, because `sliderVelocitySourceObject` will have been pointing at the correct object already,
|
||||||
|
// but to decrease unnecessary work being done every frame, the invalidation is explicitly re-triggered to update the toolbox state.
|
||||||
|
if (!sliderVelocitySourceObject.IsValid)
|
||||||
|
{
|
||||||
|
var lastSlider = getLastSlider();
|
||||||
|
sliderVelocitySourceObject.Value = lastSlider;
|
||||||
|
|
||||||
|
if (lastSlider == null)
|
||||||
|
{
|
||||||
|
useLastSliderButton.Enabled.Value = false;
|
||||||
|
useLastSliderButton.ExpandedLabelText = "No sliders to get velocity from";
|
||||||
|
useLastSliderButton.ContractedLabelText = default;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
useLastSliderButton.Enabled.Value = useLastSliderButton.Expanded.Value && !useLastSliderVelocity;
|
||||||
|
useLastSliderButton.ExpandedLabelText = useLastSliderVelocity
|
||||||
|
? "Using last slider's velocity"
|
||||||
|
: LocalisableString.Interpolate($@"Use last slider's velocity ({lastSlider.SliderVelocityMultiplier.ToLocalisableString("N2")}x)");
|
||||||
|
useLastSliderButton.ContractedLabelText = $@"current {lastSlider.SliderVelocityMultiplier.ToLocalisableString("N2")}x";
|
||||||
|
if (useLastSliderVelocity)
|
||||||
|
sliderVelocity.Value = lastSlider.SliderVelocityMultiplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Slider? getLastSlider()
|
||||||
|
{
|
||||||
|
return editorBeatmap
|
||||||
|
.HitObjects
|
||||||
|
.OfType<Slider>()
|
||||||
|
.LastOrDefault(h => h.StartTime <= editorClock.CurrentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
if (editorBeatmap.IsNotNull())
|
||||||
|
{
|
||||||
|
editorBeatmap.HitObjectAdded -= invalidateSliderVelocitySourceObject;
|
||||||
|
editorBeatmap.HitObjectUpdated -= invalidateSliderVelocitySourceObject;
|
||||||
|
editorBeatmap.HitObjectRemoved -= invalidateSliderVelocitySourceObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
{
|
{
|
||||||
public abstract partial class InputBlockingMod : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IUpdatableByPlayfield
|
public abstract partial class InputBlockingMod : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IUpdatableByPlayfield
|
||||||
{
|
{
|
||||||
public override double ScoreMultiplier => 1.0;
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(OsuModCinema) };
|
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(OsuModCinema) };
|
||||||
public override ModType Type => ModType.Conversion;
|
public override ModType Type => ModType.Conversion;
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override string Name => "Approach Different";
|
public override string Name => "Approach Different";
|
||||||
public override string Acronym => "AD";
|
public override string Acronym => "AD";
|
||||||
public override LocalisableString Description => "Never trust the approach circles...";
|
public override LocalisableString Description => "Never trust the approach circles...";
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
public override IconUsage? Icon => OsuIcon.ModApproachDifferent;
|
public override IconUsage? Icon => OsuIcon.ModApproachDifferent;
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModFreezeFrame) };
|
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModFreezeFrame) };
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override IconUsage? Icon => OsuIcon.ModAutopilot;
|
public override IconUsage? Icon => OsuIcon.ModAutopilot;
|
||||||
public override ModType Type => ModType.Automation;
|
public override ModType Type => ModType.Automation;
|
||||||
public override LocalisableString Description => @"Automatic cursor movement - just follow the rhythm.";
|
public override LocalisableString Description => @"Automatic cursor movement - just follow the rhythm.";
|
||||||
public override double ScoreMultiplier => 0.1;
|
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => new[]
|
public override Type[] IncompatibleMods => new[]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override IconUsage? Icon => OsuIcon.ModBlinds;
|
public override IconUsage? Icon => OsuIcon.ModBlinds;
|
||||||
public override ModType Type => ModType.DifficultyIncrease;
|
public override ModType Type => ModType.DifficultyIncrease;
|
||||||
|
|
||||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight) };
|
public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight) };
|
||||||
public override bool Ranked => true;
|
public override bool Ranked => true;
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override IconUsage? Icon => OsuIcon.ModBloom;
|
public override IconUsage? Icon => OsuIcon.ModBloom;
|
||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
public override LocalisableString Description => "The cursor blooms into.. a larger cursor!";
|
public override LocalisableString Description => "The cursor blooms into.. a larger cursor!";
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
protected const float MIN_SIZE = 1;
|
protected const float MIN_SIZE = 1;
|
||||||
protected const float TRANSITION_DURATION = 100;
|
protected const float TRANSITION_DURATION = 100;
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight), typeof(OsuModNoScope), typeof(ModTouchDevice) };
|
public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight), typeof(OsuModNoScope), typeof(ModTouchDevice) };
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
public override LocalisableString Description => "Don't let their popping distract you!";
|
public override LocalisableString Description => "Don't let their popping distract you!";
|
||||||
|
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
|
|
||||||
public override IconUsage? Icon => OsuIcon.ModBubbles;
|
public override IconUsage? Icon => OsuIcon.ModBubbles;
|
||||||
|
|
||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override IconUsage? Icon => OsuIcon.ModDepth;
|
public override IconUsage? Icon => OsuIcon.ModDepth;
|
||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
public override LocalisableString Description => "3D. Almost.";
|
public override LocalisableString Description => "3D. Almost.";
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame), typeof(ModWithVisibilityAdjustment) }).ToArray();
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame), typeof(ModWithVisibilityAdjustment) }).ToArray();
|
||||||
|
|
||||||
private static readonly Vector3 camera_position = new Vector3(OsuPlayfield.BASE_SIZE.X * 0.5f, OsuPlayfield.BASE_SIZE.Y * 0.5f, -200);
|
private static readonly Vector3 camera_position = new Vector3(OsuPlayfield.BASE_SIZE.X * 0.5f, OsuPlayfield.BASE_SIZE.Y * 0.5f, -200);
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
{
|
{
|
||||||
public partial class OsuModFlashlight : ModFlashlight<OsuHitObject>, IApplicableToDrawableHitObject
|
public partial class OsuModFlashlight : ModFlashlight<OsuHitObject>, IApplicableToDrawableHitObject
|
||||||
{
|
{
|
||||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
|
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModBloom), typeof(OsuModBlinds) }).ToArray();
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModBloom), typeof(OsuModBlinds) }).ToArray();
|
||||||
|
|
||||||
private const double default_follow_delay = 120;
|
private const double default_follow_delay = 120;
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
public override IconUsage? Icon => OsuIcon.ModFreezeFrame;
|
public override IconUsage? Icon => OsuIcon.ModFreezeFrame;
|
||||||
|
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
|
|
||||||
public override LocalisableString Description => "Burn the notes into your memory.";
|
public override LocalisableString Description => "Burn the notes into your memory.";
|
||||||
|
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
{
|
{
|
||||||
public class OsuModHardRock : ModHardRock, IApplicableToHitObject
|
public class OsuModHardRock : ModHardRock, IApplicableToHitObject
|
||||||
{
|
{
|
||||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
|
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModMirror)).ToArray();
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModMirror)).ToArray();
|
||||||
|
|
||||||
public void ApplyToHitObject(HitObject hitObject)
|
public void ApplyToHitObject(HitObject hitObject)
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public Bindable<bool> OnlyFadeApproachCircles { get; } = new BindableBool();
|
public Bindable<bool> OnlyFadeApproachCircles { get; } = new BindableBool();
|
||||||
|
|
||||||
public override LocalisableString Description => @"Play with no approach circles and fading circles/sliders.";
|
public override LocalisableString Description => @"Play with no approach circles and fading circles/sliders.";
|
||||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
|
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModDepth), typeof(OsuModFreezeFrame) };
|
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModDepth), typeof(OsuModFreezeFrame) };
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override IconUsage? Icon => OsuIcon.ModMagnetised;
|
public override IconUsage? Icon => OsuIcon.ModMagnetised;
|
||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
public override LocalisableString Description => "No need to chase the circles – your cursor is a magnet!";
|
public override LocalisableString Description => "No need to chase the circles – your cursor is a magnet!";
|
||||||
public override double ScoreMultiplier => 0.5;
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles), typeof(OsuModDepth) };
|
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles), typeof(OsuModDepth) };
|
||||||
|
|
||||||
[SettingSource("Attraction strength", "How strong the pull is.", 0)]
|
[SettingSource("Attraction strength", "How strong the pull is.", 0)]
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
{
|
{
|
||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
|
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
|
|
||||||
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
|
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
|
||||||
public abstract BindableNumber<float> StartScale { get; }
|
public abstract BindableNumber<float> StartScale { get; }
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override IconUsage? Icon => OsuIcon.ModRepel;
|
public override IconUsage? Icon => OsuIcon.ModRepel;
|
||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
public override LocalisableString Description => "Hit objects run away!";
|
public override LocalisableString Description => "Hit objects run away!";
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModBubbles), typeof(OsuModDepth) };
|
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModBubbles), typeof(OsuModDepth) };
|
||||||
|
|
||||||
[SettingSource("Repulsion strength", "How strong the repulsion is.", 0)]
|
[SettingSource("Repulsion strength", "How strong the repulsion is.", 0)]
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override IconUsage? Icon => OsuIcon.ModSpinIn;
|
public override IconUsage? Icon => OsuIcon.ModSpinIn;
|
||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
public override LocalisableString Description => "Circles spin in. No approach circles.";
|
public override LocalisableString Description => "Circles spin in. No approach circles.";
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
|
|
||||||
// todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque,
|
// todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque,
|
||||||
// further implementation will be required for supporting that.
|
// further implementation will be required for supporting that.
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override IconUsage? Icon => OsuIcon.ModSpunOut;
|
public override IconUsage? Icon => OsuIcon.ModSpunOut;
|
||||||
public override ModType Type => ModType.Automation;
|
public override ModType Type => ModType.Automation;
|
||||||
public override LocalisableString Description => @"Spinners will be automatically completed.";
|
public override LocalisableString Description => @"Spinners will be automatically completed.";
|
||||||
public override double ScoreMultiplier => 0.9;
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTargetPractice) };
|
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTargetPractice) };
|
||||||
public override bool Ranked => UsesDefaultConfiguration;
|
public override bool Ranked => UsesDefaultConfiguration;
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override IconUsage? Icon => OsuIcon.ModStrictTracking;
|
public override IconUsage? Icon => OsuIcon.ModStrictTracking;
|
||||||
public override ModType Type => ModType.DifficultyIncrease;
|
public override ModType Type => ModType.DifficultyIncrease;
|
||||||
public override LocalisableString Description => @"Once you start a slider, follow precisely or get a miss.";
|
public override LocalisableString Description => @"Once you start a slider, follow precisely or get a miss.";
|
||||||
public override double ScoreMultiplier => 1.0;
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTargetPractice) };
|
public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTargetPractice) };
|
||||||
|
|
||||||
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
|
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override ModType Type => ModType.Conversion;
|
public override ModType Type => ModType.Conversion;
|
||||||
public override IconUsage? Icon => OsuIcon.ModTargetPractice;
|
public override IconUsage? Icon => OsuIcon.ModTargetPractice;
|
||||||
public override LocalisableString Description => @"Practice keeping up with the beat of the song.";
|
public override LocalisableString Description => @"Practice keeping up with the beat of the song.";
|
||||||
public override double ScoreMultiplier => 0.1;
|
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override IconUsage? Icon => OsuIcon.ModTraceable;
|
public override IconUsage? Icon => OsuIcon.ModTraceable;
|
||||||
public override ModType Type => ModType.DifficultyIncrease;
|
public override ModType Type => ModType.DifficultyIncrease;
|
||||||
public override LocalisableString Description => "Put your faith in the approach circles...";
|
public override LocalisableString Description => "Put your faith in the approach circles...";
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
public override bool Ranked => true;
|
public override bool Ranked => true;
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) };
|
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) };
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override IconUsage? Icon => OsuIcon.ModTransform;
|
public override IconUsage? Icon => OsuIcon.ModTransform;
|
||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
public override LocalisableString Description => "Everything rotates. EVERYTHING.";
|
public override LocalisableString Description => "Everything rotates. EVERYTHING.";
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame), typeof(OsuModDepth) }).ToArray();
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame), typeof(OsuModDepth) }).ToArray();
|
||||||
|
|
||||||
private float theta;
|
private float theta;
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override IconUsage? Icon => OsuIcon.ModWiggle;
|
public override IconUsage? Icon => OsuIcon.ModWiggle;
|
||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
public override LocalisableString Description => "They just won't stay still...";
|
public override LocalisableString Description => "They just won't stay still...";
|
||||||
public override double ScoreMultiplier => 1;
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModDepth) };
|
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModDepth) };
|
||||||
|
|
||||||
private const int wiggle_duration = 100; // (ms) Higher = fewer wiggles
|
private const int wiggle_duration = 100; // (ms) Higher = fewer wiggles
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user