mirror of
https://github.com/ppy/osu.git
synced 2026-06-05 18:47:02 +08:00
Compare commits
14 Commits
2026.522.0
...
2026.605.0
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.521.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.527.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
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;
|
||||
@@ -42,29 +43,7 @@ namespace osu.Game.Benchmarks
|
||||
public override void SetUp()
|
||||
{
|
||||
base.SetUp();
|
||||
calculator = new OsuRuleset().CreateScoreMultiplierCalculator();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public double ViaModScoreMultiplier() => viaModScoreMultiplier(Times, Mods);
|
||||
|
||||
[Test]
|
||||
public void ViaModScoreMultiplier([Values(100)] int times, [ValueSource(nameof(ValuesForMods))] ModTestCase mods)
|
||||
=> viaModScoreMultiplier(times, mods);
|
||||
|
||||
private double viaModScoreMultiplier(int times, ModTestCase mods)
|
||||
{
|
||||
double scoreMultiplier = 1;
|
||||
|
||||
for (int i = 0; i < times; ++i)
|
||||
{
|
||||
scoreMultiplier = 1;
|
||||
|
||||
foreach (var mod in mods.Mods)
|
||||
scoreMultiplier *= mod.ScoreMultiplier;
|
||||
}
|
||||
|
||||
return scoreMultiplier;
|
||||
calculator = new OsuRuleset().CreateScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty()));
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
// 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
|
||||
@@ -14,28 +19,153 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFlashlightOnNonDefaultSettings()
|
||||
=> TestModCombination([new CatchModFlashlight { ComboBasedSize = { Value = false } }]);
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
#region Difficulty Reduction
|
||||
|
||||
[Test]
|
||||
public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
|
||||
=> TestModCombination([new CatchModHalfTime { SpeedChange = { Value = speedChange } }]);
|
||||
[new Mod[] { new CatchModEasy() }, 0.5],
|
||||
[new Mod[] { new CatchModNoFail() }, 0.5],
|
||||
|
||||
[Test]
|
||||
public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
|
||||
=> TestModCombination([new CatchModDaycore { SpeedChange = { Value = speedChange } }]);
|
||||
[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],
|
||||
|
||||
[Test]
|
||||
public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
|
||||
=> TestModCombination([new CatchModDoubleTime { SpeedChange = { Value = speedChange } }]);
|
||||
[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],
|
||||
|
||||
[Test]
|
||||
public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
|
||||
=> TestModCombination([new CatchModNightcore { SpeedChange = { Value = speedChange } }]);
|
||||
#endregion
|
||||
|
||||
[Test]
|
||||
public void TestMultiplicativeCombination()
|
||||
=> TestModCombination([new CatchModHidden(), new CatchModHardRock()]);
|
||||
#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,7 +169,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
}
|
||||
}
|
||||
|
||||
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new CatchScoreMultiplierCalculator();
|
||||
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new CatchScoreMultiplierCalculator(context);
|
||||
|
||||
public override string Description => "osu!catch";
|
||||
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
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();
|
||||
|
||||
public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject)
|
||||
|
||||
@@ -13,8 +13,6 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public partial class CatchModFlashlight : ModFlashlight<CatchHitObject>
|
||||
{
|
||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
|
||||
|
||||
public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
|
||||
{
|
||||
MinValue = 0.5f,
|
||||
|
||||
@@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
public override string Name => "Floating Fruits";
|
||||
public override string Acronym => "FF";
|
||||
public override LocalisableString Description => "The fruits are... floating?";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override IconUsage? Icon => OsuIcon.ModFloatingFruits;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
||||
|
||||
@@ -10,8 +10,6 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModHardRock : ModHardRock, IApplicableToBeatmapProcessor
|
||||
{
|
||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
|
||||
|
||||
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
|
||||
{
|
||||
var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
|
||||
|
||||
@@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset<CatchHitObject>
|
||||
{
|
||||
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_duration_multiplier = 0.44;
|
||||
|
||||
@@ -23,7 +23,6 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
public override string Acronym => "MF";
|
||||
public override LocalisableString Description => "Dashing by default, slow down!";
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override IconUsage? Icon => OsuIcon.ModMovingFast;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) };
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
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
|
||||
{
|
||||
static CatchScoreMultiplierCalculator()
|
||||
public CatchScoreMultiplierCalculator(ScoreMultiplierContext context)
|
||||
: base(context)
|
||||
{
|
||||
#region Difficulty Reduction
|
||||
|
||||
@@ -36,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
#region Conversion
|
||||
|
||||
Single<CatchModDifficultyAdjust>(hasMultiplier: 0.5);
|
||||
Single<CatchModClassic>(hasMultiplier: 0.96);
|
||||
Single<CatchModClassic>(hasMultiplier: _ => classicMultiplier(context.Score));
|
||||
// Mirror
|
||||
|
||||
#endregion
|
||||
@@ -81,5 +83,13 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
else
|
||||
return 0.6 + value;
|
||||
}
|
||||
|
||||
private static double classicMultiplier(ScoreInfo? score)
|
||||
{
|
||||
if (score != null && score.TotalScoreVersion < 30000017)
|
||||
return 0.96;
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using 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
|
||||
@@ -14,24 +20,217 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
|
||||
=> TestModCombination([new ManiaModHalfTime { SpeedChange = { Value = speedChange } }]);
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
#region Difficulty Reduction
|
||||
|
||||
[Test]
|
||||
public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
|
||||
=> TestModCombination([new ManiaModDaycore { SpeedChange = { Value = speedChange } }]);
|
||||
[new Mod[] { new ManiaModEasy() }, 0.5],
|
||||
[new Mod[] { new ManiaModNoFail() }, 0.5],
|
||||
|
||||
[Test]
|
||||
public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
|
||||
=> TestModCombination([new ManiaModDoubleTime { SpeedChange = { Value = speedChange } }]);
|
||||
[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],
|
||||
|
||||
[Test]
|
||||
public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
|
||||
=> TestModCombination([new ManiaModNightcore { SpeedChange = { Value = speedChange } }]);
|
||||
[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],
|
||||
|
||||
[Test]
|
||||
public void TestMultiplicativeCombination()
|
||||
=> TestModCombination([new ManiaModEasy(), new ManiaModKey4()]);
|
||||
[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.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
@@ -54,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
Mod = doubleTime,
|
||||
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0
|
||||
&& 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,
|
||||
CreateBeatmap = () => new Beatmap
|
||||
{
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
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)
|
||||
{
|
||||
switch (hitObject)
|
||||
|
||||
@@ -307,7 +307,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
}
|
||||
}
|
||||
|
||||
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new ManiaScoreMultiplierCalculator();
|
||||
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new ManiaScoreMultiplierCalculator(context);
|
||||
|
||||
public override string Description => "osu!mania";
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override string Acronym => Name;
|
||||
public abstract int KeyCount { get; }
|
||||
public override ModType Type => ModType.Conversion;
|
||||
public override double ScoreMultiplier => 0.9;
|
||||
public override bool Ranked => UsesDefaultConfiguration;
|
||||
|
||||
public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
|
||||
|
||||
@@ -18,8 +18,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
public override string Acronym => "CS";
|
||||
|
||||
public override double ScoreMultiplier => 0.9;
|
||||
|
||||
public override LocalisableString Description => "No more tricky speed changes!";
|
||||
|
||||
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 double ScoreMultiplier => 1;
|
||||
|
||||
protected override CoverExpandDirection ExpandDirection => Direction.Value;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
||||
|
||||
@@ -7,9 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
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 IconUsage? Icon => OsuIcon.ModDualStages;
|
||||
public override ModType Type => ModType.Conversion;
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
private bool isForCurrentRuleset;
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override string Acronym => "FI";
|
||||
public override IconUsage? Icon => OsuIcon.ModFadeIn;
|
||||
public override LocalisableString Description => @"Keys appear out of nowhere!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool ValidForFreestyleAsRequiredMod => false;
|
||||
|
||||
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 override double ScoreMultiplier => 1;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModHidden) };
|
||||
|
||||
public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
|
||||
|
||||
@@ -10,7 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModHardRock : ModHardRock, IApplicableToHitObject
|
||||
{
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool Ranked => false;
|
||||
|
||||
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;
|
||||
|
||||
public override LocalisableString Description => @"Keys fade out before you hit them!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
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 double ScoreMultiplier => 0.9;
|
||||
|
||||
public override LocalisableString Description => @"Replaces all hold notes with normal notes.";
|
||||
|
||||
public override IconUsage? Icon => OsuIcon.ModHoldOff;
|
||||
|
||||
@@ -20,7 +20,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override string Name => "Invert";
|
||||
|
||||
public override string Acronym => "IN";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
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
|
||||
{
|
||||
// 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 double ScoreMultiplier => 0.9;
|
||||
|
||||
public override IconUsage? Icon => OsuIcon.ModNoRelease;
|
||||
|
||||
public override ModType Type => ModType.DifficultyReduction;
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using 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
|
||||
{
|
||||
static ManiaScoreMultiplierCalculator()
|
||||
public ManiaScoreMultiplierCalculator(ScoreMultiplierContext context)
|
||||
: base(context)
|
||||
{
|
||||
#region Difficulty Reduction
|
||||
|
||||
@@ -42,20 +45,20 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
// Dual Stages
|
||||
// Mirror
|
||||
Single<ManiaModDifficultyAdjust>(hasMultiplier: 0.5);
|
||||
Single<ManiaModClassic>(hasMultiplier: 0.96);
|
||||
Single<ManiaModClassic>(hasMultiplier: _ => classicMultiplier(Context.Score));
|
||||
// Invert
|
||||
Single<ManiaModConstantSpeed>(hasMultiplier: 0.9);
|
||||
Single<ManiaModHoldOff>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey1>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey2>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey3>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey4>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey5>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey6>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey7>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey8>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey9>(hasMultiplier: 0.9);
|
||||
Single<ManiaModKey10>(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
|
||||
|
||||
@@ -95,5 +98,57 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneOsuModDifficultyAdjust : OsuModTestScene
|
||||
{
|
||||
protected override bool AllowFail => true;
|
||||
|
||||
[Test]
|
||||
public void TestNoAdjustment() => CreateModTest(new ModTestData
|
||||
{
|
||||
@@ -72,6 +74,88 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
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)
|
||||
{
|
||||
var objects = Player.ChildrenOfType<DrawableHitCircle>();
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
// 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
|
||||
@@ -14,32 +19,279 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFlashlightOnNonDefaultSettings()
|
||||
=> TestModCombination([new OsuModFlashlight { ComboBasedSize = { Value = false } }]);
|
||||
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 TestHiddenOnNonDefaultSettings()
|
||||
=> TestModCombination([new OsuModHidden { OnlyFadeApproachCircles = { Value = true } }]);
|
||||
|
||||
[Test]
|
||||
public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
|
||||
=> TestModCombination([new OsuModHalfTime { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
|
||||
=> TestModCombination([new OsuModDaycore { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
|
||||
=> TestModCombination([new OsuModDoubleTime { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
|
||||
=> TestModCombination([new OsuModNightcore { SpeedChange = { Value = speedChange } }]);
|
||||
|
||||
[Test]
|
||||
public void TestMultiplicativeCombination()
|
||||
=> TestModCombination([new OsuModHidden(), new OsuModHardRock()]);
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
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 ModType Type => ModType.Conversion;
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override string Name => "Approach Different";
|
||||
public override string Acronym => "AD";
|
||||
public override LocalisableString Description => "Never trust the approach circles...";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override IconUsage? Icon => OsuIcon.ModApproachDifferent;
|
||||
|
||||
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 ModType Type => ModType.Automation;
|
||||
public override LocalisableString Description => @"Automatic cursor movement - just follow the rhythm.";
|
||||
public override double ScoreMultiplier => 0.1;
|
||||
|
||||
public override Type[] IncompatibleMods => new[]
|
||||
{
|
||||
|
||||
@@ -30,7 +30,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModBlinds;
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
|
||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight) };
|
||||
public override bool Ranked => true;
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModBloom;
|
||||
public override ModType Type => ModType.Fun;
|
||||
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 TRANSITION_DURATION = 100;
|
||||
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 double ScoreMultiplier => 1;
|
||||
|
||||
public override IconUsage? Icon => OsuIcon.ModBubbles;
|
||||
|
||||
public override ModType Type => ModType.Fun;
|
||||
|
||||
@@ -25,7 +25,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModDepth;
|
||||
public override ModType Type => ModType.Fun;
|
||||
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();
|
||||
|
||||
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 override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModBloom), typeof(OsuModBlinds) }).ToArray();
|
||||
|
||||
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 double ScoreMultiplier => 1;
|
||||
|
||||
public override LocalisableString Description => "Burn the notes into your memory.";
|
||||
|
||||
/// <remarks>
|
||||
|
||||
@@ -13,8 +13,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModHardRock : ModHardRock, IApplicableToHitObject
|
||||
{
|
||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModMirror)).ToArray();
|
||||
|
||||
public void ApplyToHitObject(HitObject hitObject)
|
||||
|
||||
@@ -24,7 +24,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public Bindable<bool> OnlyFadeApproachCircles { get; } = new BindableBool();
|
||||
|
||||
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) };
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModMagnetised;
|
||||
public override ModType Type => ModType.Fun;
|
||||
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) };
|
||||
|
||||
[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 double ScoreMultiplier => 1;
|
||||
|
||||
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
|
||||
public abstract BindableNumber<float> StartScale { get; }
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModRepel;
|
||||
public override ModType Type => ModType.Fun;
|
||||
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) };
|
||||
|
||||
[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 ModType Type => ModType.Fun;
|
||||
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,
|
||||
// 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 ModType Type => ModType.Automation;
|
||||
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 bool Ranked => UsesDefaultConfiguration;
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModStrictTracking;
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
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 void ApplyToDrawableHitObject(DrawableHitObject drawable)
|
||||
|
||||
@@ -39,7 +39,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override ModType Type => ModType.Conversion;
|
||||
public override IconUsage? Icon => OsuIcon.ModTargetPractice;
|
||||
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[]
|
||||
{
|
||||
|
||||
@@ -23,7 +23,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModTraceable;
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override LocalisableString Description => "Put your faith in the approach circles...";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool Ranked => true;
|
||||
|
||||
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 ModType Type => ModType.Fun;
|
||||
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();
|
||||
|
||||
private float theta;
|
||||
|
||||
@@ -23,7 +23,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModWiggle;
|
||||
public override ModType Type => ModType.Fun;
|
||||
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) };
|
||||
|
||||
private const int wiggle_duration = 100; // (ms) Higher = fewer wiggles
|
||||
|
||||
@@ -234,7 +234,13 @@ namespace osu.Game.Rulesets.Osu
|
||||
}
|
||||
}
|
||||
|
||||
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new OsuScoreMultiplierCalculator();
|
||||
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context)
|
||||
{
|
||||
if (context.Score != null && context.Score.TotalScoreVersion < 30000017)
|
||||
return new OsuScoreMultiplierCalculatorV1(context);
|
||||
|
||||
return new OsuScoreMultiplierCalculatorV2(context);
|
||||
}
|
||||
|
||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetOsu };
|
||||
|
||||
|
||||
+3
-2
@@ -7,9 +7,10 @@ using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Scoring
|
||||
{
|
||||
public class OsuScoreMultiplierCalculator : ScoreMultiplierCalculator
|
||||
public class OsuScoreMultiplierCalculatorV1 : ScoreMultiplierCalculator
|
||||
{
|
||||
static OsuScoreMultiplierCalculator()
|
||||
public OsuScoreMultiplierCalculatorV1(ScoreMultiplierContext context)
|
||||
: base(context)
|
||||
{
|
||||
#region Difficulty Reduction
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
// 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.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Scoring
|
||||
{
|
||||
public class OsuScoreMultiplierCalculatorV2 : ScoreMultiplierCalculator
|
||||
{
|
||||
public OsuScoreMultiplierCalculatorV2(ScoreMultiplierContext context)
|
||||
: base(context)
|
||||
{
|
||||
#region Difficulty Reduction
|
||||
|
||||
Single<OsuModEasy>(hasMultiplier: easyMultiplier);
|
||||
Single<OsuModNoFail>(hasMultiplier: 0.5);
|
||||
Single<OsuModHalfTime>(hasMultiplier: halfTime => halfTimeMultiplier(halfTime.SpeedChange.Value));
|
||||
Single<OsuModDaycore>(hasMultiplier: daycore => halfTimeMultiplier(daycore.SpeedChange.Value));
|
||||
|
||||
#endregion
|
||||
|
||||
#region Difficulty Increase
|
||||
|
||||
Single<OsuModHardRock>(hasMultiplier: 1.09);
|
||||
// Sudden Death (1.0x)
|
||||
// Perfect (1.0x)
|
||||
Single<OsuModDoubleTime>(hasMultiplier: doubleTime => doubleTimeMultiplier(doubleTime.SpeedChange.Value));
|
||||
Single<OsuModNightcore>(hasMultiplier: nightcore => doubleTimeMultiplier(nightcore.SpeedChange.Value));
|
||||
|
||||
const double blinds_multiplier = 1.24;
|
||||
|
||||
Combination<OsuModHidden, OsuModBlinds>(hasMultiplier: (_, _) => blinds_multiplier);
|
||||
|
||||
Combination<OsuModHidden, OsuModWiggle>(hasMultiplier: (hidden, _) => hiddenMultiplier(hidden, otherModsProvideTimingInfo: true));
|
||||
Combination<OsuModHidden, OsuModGrow>(hasMultiplier: (hidden, _) => hiddenMultiplier(hidden, otherModsProvideTimingInfo: true));
|
||||
Combination<OsuModHidden, OsuModDeflate>(hasMultiplier: (hidden, deflate) => hiddenMultiplier(hidden, otherModsProvideTimingInfo: true) * deflateMultiplier(deflate));
|
||||
Combination<OsuModHidden, OsuModRepel>(hasMultiplier: (hidden, _) => hiddenMultiplier(hidden, otherModsProvideTimingInfo: true));
|
||||
Combination<OsuModHidden, OsuModDepth>(hasMultiplier: (hidden, _) => hiddenMultiplier(hidden, otherModsProvideTimingInfo: true));
|
||||
|
||||
Single<OsuModHidden>(hasMultiplier: hidden => hiddenMultiplier(hidden, otherModsProvideTimingInfo: false));
|
||||
|
||||
Combination<OsuModTraceable, OsuModBlinds>(hasMultiplier: (_, _) => blinds_multiplier);
|
||||
Single<OsuModTraceable>(hasMultiplier: 1.02);
|
||||
|
||||
Combination<OsuModFlashlight, OsuModFreezeFrame>(hasMultiplier: (flashlight, _) => 1 + (flashlightMultiplier(flashlight) - 1) / 2);
|
||||
Single<OsuModFlashlight>(hasMultiplier: flashlightMultiplier);
|
||||
|
||||
Single<OsuModBlinds>(hasMultiplier: blinds_multiplier);
|
||||
// Strict Tracking (1.0x)
|
||||
// Accuracy Challenge (1.0x)
|
||||
|
||||
#endregion
|
||||
|
||||
#region Conversion
|
||||
|
||||
Single<OsuModTargetPractice>(hasMultiplier: 0.01);
|
||||
Single<OsuModDifficultyAdjust>(hasMultiplier: difficultyAdjust => difficultyAdjustMultiplier(difficultyAdjust, Context.BeatmapDifficultyWithoutMods));
|
||||
Single<OsuModClassic>(hasMultiplier: classic => classic.ClassicNoteLock.Value ? 0.985 : 0.96);
|
||||
Single<OsuModRandom>(hasMultiplier: 0.7);
|
||||
// Mirror (1.0x)
|
||||
// Alternate (1.0x)
|
||||
// Single Tap (1.0x)
|
||||
|
||||
#endregion
|
||||
|
||||
#region Automation
|
||||
|
||||
// Autoplay (1.0x)
|
||||
// Cinema (1.0x)
|
||||
Single<OsuModRelax>(hasMultiplier: 0.1);
|
||||
Single<OsuModAutopilot>(hasMultiplier: 0.1);
|
||||
Single<OsuModSpunOut>(hasMultiplier: 0.95);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fun
|
||||
|
||||
// Transform (1.0x)
|
||||
// Wiggle (1.0x)
|
||||
// Spin In (1.0x)
|
||||
// Grow (1.0x)
|
||||
Single<OsuModDeflate>(hasMultiplier: deflateMultiplier);
|
||||
Single<ModWindUp>(hasMultiplier: timeRampMultiplier);
|
||||
Single<ModWindDown>(hasMultiplier: timeRampMultiplier);
|
||||
// Barrel Roll (1.0x)
|
||||
Single<OsuModApproachDifferent>(hasMultiplier: 0.7);
|
||||
// Muted (1.0x)
|
||||
// No Scope (1.0x)
|
||||
Single<OsuModMagnetised>(hasMultiplier: magnetised => 0.7 - magnetised.AttractionStrength.Value * 0.6);
|
||||
// Repel (1.0x)
|
||||
Single<ModAdaptiveSpeed>(hasMultiplier: 0.1);
|
||||
// Freeze Frame (1.0x)
|
||||
// Bubbles (1.0x)
|
||||
Single<OsuModSynesthesia>(hasMultiplier: 0.99);
|
||||
// Depth (1.0x)
|
||||
// Bloom (1.0x)
|
||||
|
||||
#endregion
|
||||
|
||||
#region System
|
||||
|
||||
// Touch Device (1.0x)
|
||||
// Score V2 (1.0x)
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
private static double easyMultiplier(OsuModEasy easy)
|
||||
{
|
||||
// 0.8x base multiplier
|
||||
// Reduce by 0.1x per extra life
|
||||
double value = 0.8 - Math.Max(0, 0.1 * (easy.Retries.Value - easy.Retries.Default));
|
||||
|
||||
return Math.Max(0.4, value);
|
||||
}
|
||||
|
||||
private static double halfTimeMultiplier(double speedChange)
|
||||
{
|
||||
// 0.2x at 0.5x speed, +0.07x per 0.05x speed increment.
|
||||
// Default HT (0.75x) = 0.55
|
||||
return (int)(speedChange * 20) / 20.0 * 1.4 - 0.5;
|
||||
}
|
||||
|
||||
private static double doubleTimeMultiplier(double speedChange)
|
||||
{
|
||||
// Floor to the nearest multiple of 0.1.
|
||||
double value = (int)(speedChange * 10) / 10.0;
|
||||
|
||||
// 0.01 penalty for non-default rates.
|
||||
double penalty = value != 1.5 && value != 1.0 ? 0.01 : 0.0;
|
||||
|
||||
// Linear from 1.0 to 1.46, minus the penalty.
|
||||
// Default DT (1.5x) = 1.23
|
||||
return (value - 1) * 0.46 + 1 - penalty;
|
||||
}
|
||||
|
||||
private static double hiddenMultiplier(OsuModHidden hidden, bool otherModsProvideTimingInfo)
|
||||
{
|
||||
double value = 1.04;
|
||||
|
||||
if (hidden.OnlyFadeApproachCircles.Value)
|
||||
value -= 0.02;
|
||||
|
||||
if (otherModsProvideTimingInfo)
|
||||
value -= 0.02;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static double flashlightMultiplier(OsuModFlashlight flashlight)
|
||||
{
|
||||
// Multiplier of 1.2x, reduced by 0.02 per 0.1 increase in flashlight size.
|
||||
double value = Math.Max(1.02, Math.Min(1.2, 1.2 - 0.2 * (flashlight.SizeMultiplier.Value - 1)));
|
||||
|
||||
if (!flashlight.ComboBasedSize.Value)
|
||||
value = 1 + (value - 1) / 5;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static double difficultyAdjustMultiplier(OsuModDifficultyAdjust difficultyAdjust, IBeatmapDifficultyInfo beatmapDifficulty)
|
||||
{
|
||||
double selectedCircleSize = difficultyAdjust.CircleSize.Value ?? beatmapDifficulty.CircleSize;
|
||||
double selectedDrainRate = difficultyAdjust.DrainRate.Value ?? beatmapDifficulty.DrainRate;
|
||||
double selectedOverallDifficulty = difficultyAdjust.OverallDifficulty.Value ?? beatmapDifficulty.OverallDifficulty;
|
||||
double selectedApproachRate = difficultyAdjust.ApproachRate.Value ?? beatmapDifficulty.ApproachRate;
|
||||
|
||||
double csDifference = Math.Abs(selectedCircleSize - beatmapDifficulty.CircleSize);
|
||||
double hpDifference = Math.Abs(selectedDrainRate - beatmapDifficulty.DrainRate);
|
||||
double odDifference = Math.Abs(selectedOverallDifficulty - beatmapDifficulty.OverallDifficulty);
|
||||
double arDifference = Math.Abs(selectedApproachRate - beatmapDifficulty.ApproachRate);
|
||||
|
||||
// Per parameter, reduce multiplier by 0.05x per 0.1 change.
|
||||
double csMultiplier = Math.Max(0.1, 1.0 - csDifference * 0.5);
|
||||
double hpMultiplier = Math.Max(0.1, 1.0 - hpDifference * 0.5);
|
||||
double odMultiplier = Math.Max(0.1, 1.0 - odDifference * 0.5);
|
||||
double arMultiplier = Math.Max(0.1, 1.0 - arDifference * 0.5);
|
||||
|
||||
return Math.Max(0.1, csMultiplier * hpMultiplier * odMultiplier * arMultiplier);
|
||||
}
|
||||
|
||||
private static double timeRampMultiplier(ModTimeRamp timeRamp)
|
||||
{
|
||||
double minSpeed = Math.Min(timeRamp.InitialRate.Value, timeRamp.FinalRate.Value);
|
||||
double maxSpeed = Math.Max(timeRamp.InitialRate.Value, timeRamp.FinalRate.Value);
|
||||
|
||||
double minMultiplier = minSpeed < 1 ? halfTimeMultiplier(minSpeed) : doubleTimeMultiplier(minSpeed);
|
||||
double maxMultiplier = maxSpeed < 1 ? halfTimeMultiplier(maxSpeed) : doubleTimeMultiplier(maxSpeed);
|
||||
|
||||
return 0.8 * minMultiplier + 0.2 * maxMultiplier;
|
||||
}
|
||||
|
||||
private static double deflateMultiplier(OsuModDeflate deflate)
|
||||
=> 1.0 - Math.Max(0, 0.02 * (deflate.StartScale.Value - deflate.StartScale.Default));
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,12 @@
|
||||
// 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.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Rulesets;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
@@ -14,28 +19,155 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFlashlightOnNonDefaultSettings()
|
||||
=> TestModCombination([new TaikoModFlashlight { ComboBasedSize = { Value = false } }]);
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
#region Difficulty Reduction
|
||||
|
||||
[Test]
|
||||
public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
|
||||
=> TestModCombination([new TaikoModHalfTime { SpeedChange = { Value = speedChange } }]);
|
||||
[new Mod[] { new TaikoModEasy() }, 0.5],
|
||||
[new Mod[] { new TaikoModNoFail() }, 0.5],
|
||||
|
||||
[Test]
|
||||
public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
|
||||
=> TestModCombination([new TaikoModDaycore { SpeedChange = { Value = speedChange } }]);
|
||||
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.50 } } }, 0.1],
|
||||
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.55 } } }, 0.1],
|
||||
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.60 } } }, 0.2],
|
||||
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.65 } } }, 0.2],
|
||||
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.70 } } }, 0.3],
|
||||
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.75 } } }, 0.3],
|
||||
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.80 } } }, 0.4],
|
||||
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.85 } } }, 0.4],
|
||||
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.90 } } }, 0.5],
|
||||
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.95 } } }, 0.5],
|
||||
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.99 } } }, 0.5],
|
||||
|
||||
[Test]
|
||||
public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
|
||||
=> TestModCombination([new TaikoModDoubleTime { SpeedChange = { Value = speedChange } }]);
|
||||
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.50 } } }, 0.1],
|
||||
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.55 } } }, 0.1],
|
||||
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.60 } } }, 0.2],
|
||||
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.65 } } }, 0.2],
|
||||
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.70 } } }, 0.3],
|
||||
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.75 } } }, 0.3],
|
||||
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.80 } } }, 0.4],
|
||||
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.85 } } }, 0.4],
|
||||
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.90 } } }, 0.5],
|
||||
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.95 } } }, 0.5],
|
||||
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.99 } } }, 0.5],
|
||||
|
||||
[Test]
|
||||
public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
|
||||
=> TestModCombination([new TaikoModNightcore { SpeedChange = { Value = speedChange } }]);
|
||||
[new Mod[] { new TaikoModSimplifiedRhythm() }, 0.6],
|
||||
|
||||
[Test]
|
||||
public void TestMultiplicativeCombination()
|
||||
=> TestModCombination([new TaikoModHidden(), new TaikoModHardRock()]);
|
||||
#endregion
|
||||
|
||||
#region Difficulty Increase
|
||||
|
||||
[new Mod[] { new TaikoModHardRock() }, 1.06],
|
||||
[new Mod[] { new TaikoModSuddenDeath() }, 1],
|
||||
[new Mod[] { new TaikoModPerfect() }, 1],
|
||||
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.01 } } }, 1.00],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.05 } } }, 1.00],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.10 } } }, 1.02],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.15 } } }, 1.02],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.20 } } }, 1.04],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.25 } } }, 1.04],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.30 } } }, 1.06],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.35 } } }, 1.06],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.40 } } }, 1.08],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.45 } } }, 1.08],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.50 } } }, 1.10],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.55 } } }, 1.10],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.60 } } }, 1.12],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.65 } } }, 1.12],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.70 } } }, 1.14],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.75 } } }, 1.14],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.80 } } }, 1.16],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.85 } } }, 1.16],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.90 } } }, 1.18],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.95 } } }, 1.18],
|
||||
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 2.00 } } }, 1.20],
|
||||
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.01 } } }, 1.00],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.05 } } }, 1.00],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.10 } } }, 1.02],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.15 } } }, 1.02],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.20 } } }, 1.04],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.25 } } }, 1.04],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.30 } } }, 1.06],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.35 } } }, 1.06],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.40 } } }, 1.08],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.45 } } }, 1.08],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.50 } } }, 1.10],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.55 } } }, 1.10],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.60 } } }, 1.12],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.65 } } }, 1.12],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.70 } } }, 1.14],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.75 } } }, 1.14],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.80 } } }, 1.16],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.85 } } }, 1.16],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.90 } } }, 1.18],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.95 } } }, 1.18],
|
||||
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 2.00 } } }, 1.20],
|
||||
|
||||
[new Mod[] { new TaikoModHidden() }, 1.06],
|
||||
|
||||
[new Mod[] { new TaikoModFlashlight() }, 1.12],
|
||||
[new Mod[] { new TaikoModFlashlight { ComboBasedSize = { Value = false } } }, 1],
|
||||
|
||||
[new Mod[] { new ModAccuracyChallenge() }, 1],
|
||||
|
||||
#endregion
|
||||
|
||||
#region Conversion
|
||||
|
||||
[new Mod[] { new TaikoModRandom() }, 1],
|
||||
[new Mod[] { new TaikoModDifficultyAdjust() }, 0.5],
|
||||
[new Mod[] { new TaikoModClassic() }, 1],
|
||||
[new Mod[] { new TaikoModSwap() }, 1],
|
||||
[new Mod[] { new TaikoModSingleTap() }, 1],
|
||||
[new Mod[] { new TaikoModConstantSpeed() }, 0.9],
|
||||
|
||||
#endregion
|
||||
|
||||
#region Automation
|
||||
|
||||
[new Mod[] { new TaikoModAutoplay() }, 1],
|
||||
[new Mod[] { new TaikoModCinema() }, 1],
|
||||
[new Mod[] { new TaikoModRelax() }, 0.1],
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fun
|
||||
|
||||
[new Mod[] { new ModWindUp() }, 0.5],
|
||||
[new Mod[] { new ModWindDown() }, 0.5],
|
||||
[new Mod[] { new TaikoModMuted() }, 1],
|
||||
[new Mod[] { new ModAdaptiveSpeed() }, 0.5],
|
||||
|
||||
#endregion
|
||||
|
||||
#region System
|
||||
|
||||
[new Mod[] { new ModScoreV2() }, 1],
|
||||
|
||||
#endregion
|
||||
|
||||
#region Combinations
|
||||
|
||||
[new Mod[] { new TaikoModHidden(), new TaikoModHardRock() }, 1.06 * 1.06]
|
||||
|
||||
#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 TaikoModClassic()]), Is.EqualTo(expectedMultiplier).Within(Precision.DOUBLE_EPSILON));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
@@ -21,6 +26,29 @@ namespace osu.Game.Rulesets.Taiko.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.EditorHit,
|
||||
Size = new Vector2(15),
|
||||
},
|
||||
new SpriteIcon
|
||||
{
|
||||
Icon = OsuIcon.EditorNewComboSparkles,
|
||||
Size = new Vector2(20),
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new TaikoSelectionHandler();
|
||||
|
||||
public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) =>
|
||||
|
||||
@@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public override string Name => "Constant Speed";
|
||||
public override string Acronym => "CS";
|
||||
public override double ScoreMultiplier => 0.9;
|
||||
public override LocalisableString Description => "No more tricky speed changes!";
|
||||
public override IconUsage? Icon => OsuIcon.ModConstantSpeed;
|
||||
public override ModType Type => ModType.Conversion;
|
||||
|
||||
@@ -14,8 +14,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public partial class TaikoModFlashlight : ModFlashlight<TaikoHitObject>
|
||||
{
|
||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
|
||||
|
||||
public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
|
||||
{
|
||||
MinValue = 0.5f,
|
||||
|
||||
@@ -9,8 +9,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public class TaikoModHardRock : ModHardRock
|
||||
{
|
||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier factor added to the scrolling speed.
|
||||
/// </summary>
|
||||
|
||||
@@ -17,7 +17,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
public class TaikoModHidden : ModHidden, IApplicableToDrawableRuleset<TaikoHitObject>
|
||||
{
|
||||
public override LocalisableString Description => @"Beats fade out before you hit them!";
|
||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
|
||||
|
||||
/// <summary>
|
||||
/// How far away from the hit target should hitobjects start to fade out.
|
||||
|
||||
@@ -21,7 +21,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public override string Name => "Simplified Rhythm";
|
||||
public override string Acronym => "SR";
|
||||
public override double ScoreMultiplier => 0.6;
|
||||
public override LocalisableString Description => "Simplify tricky rhythms!";
|
||||
public override IconUsage? Icon => OsuIcon.ModSimplifiedRhythm;
|
||||
public override ModType Type => ModType.DifficultyReduction;
|
||||
|
||||
@@ -30,7 +30,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
public override LocalisableString Description => @"One key for dons, one key for kats.";
|
||||
|
||||
public override bool Ranked => true;
|
||||
public override double ScoreMultiplier => 1.0;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(TaikoModCinema) };
|
||||
public override ModType Type => ModType.Conversion;
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
public override LocalisableString Description => @"Dons become kats, kats become dons";
|
||||
public override IconUsage? Icon => OsuIcon.ModSwap;
|
||||
public override ModType Type => ModType.Conversion;
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModRandom)).ToArray();
|
||||
public override bool Ranked => true;
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
{
|
||||
public class TaikoScoreMultiplierCalculator : ScoreMultiplierCalculator
|
||||
{
|
||||
static TaikoScoreMultiplierCalculator()
|
||||
public TaikoScoreMultiplierCalculator(ScoreMultiplierContext context)
|
||||
: base(context)
|
||||
{
|
||||
#region Difficulty Reduction
|
||||
|
||||
@@ -38,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
|
||||
// Random
|
||||
Single<TaikoModDifficultyAdjust>(hasMultiplier: 0.5);
|
||||
Single<TaikoModClassic>(hasMultiplier: 0.96);
|
||||
Single<TaikoModClassic>(hasMultiplier: _ => classicMultiplier(Context.Score));
|
||||
// Swap
|
||||
// Single Tap
|
||||
Single<TaikoModConstantSpeed>(hasMultiplier: 0.9);
|
||||
@@ -82,5 +84,13 @@ namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
else
|
||||
return 0.6 + value;
|
||||
}
|
||||
|
||||
private static double classicMultiplier(ScoreInfo? score)
|
||||
{
|
||||
if (score != null && score.TotalScoreVersion < 30000017)
|
||||
return 0.96;
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
}
|
||||
}
|
||||
|
||||
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new TaikoScoreMultiplierCalculator();
|
||||
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new TaikoScoreMultiplierCalculator(context);
|
||||
|
||||
public override string Description => "osu!taiko";
|
||||
|
||||
|
||||
@@ -552,6 +552,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("Temporarily ignored pending correct addressing of treatment of `TotalScoreVersion` in replays")]
|
||||
public void TestTotalScoreWithoutModsBackwardsPopulatedIfMissing()
|
||||
{
|
||||
var ruleset = new OsuRuleset().RulesetInfo;
|
||||
|
||||
@@ -122,6 +122,29 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
() => Is.EqualTo(384).Within(0.00001));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBackgroundSpecificationPreserved()
|
||||
{
|
||||
IWorkingBeatmap beatmap = null!;
|
||||
MemoryStream outStream = null!;
|
||||
|
||||
// Ensure importer encoding is correct
|
||||
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"241526 Soleily - Renatus.osz"));
|
||||
AddAssert("beatmap background is correct", () => beatmap.BeatmapInfo.Metadata.BackgroundFile, () => Is.EqualTo("machinetop_background.jpg"));
|
||||
|
||||
// Ensure exporter legacy conversion is correct
|
||||
AddStep("export", () =>
|
||||
{
|
||||
outStream = new MemoryStream();
|
||||
|
||||
new LegacyBeatmapExporter(LocalStorage)
|
||||
.ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
|
||||
});
|
||||
|
||||
AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
|
||||
AddAssert("beatmap background is still correct", () => beatmap.BeatmapInfo.Metadata.BackgroundFile, () => Is.EqualTo("machinetop_background.jpg"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExportStability()
|
||||
{
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
AccuracyJudgementCount = 1,
|
||||
ComboPortion = 0,
|
||||
BonusPortion = 0
|
||||
}, DateTimeOffset.Now)
|
||||
}, DateTimeOffset.Now, [], 0, [])
|
||||
});
|
||||
|
||||
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
|
||||
@@ -99,7 +99,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
AccuracyJudgementCount = 0,
|
||||
ComboPortion = 0,
|
||||
BonusPortion = 0
|
||||
}, DateTimeOffset.Now)
|
||||
}, DateTimeOffset.Now, [], 0, [])
|
||||
});
|
||||
|
||||
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
|
||||
|
||||
@@ -91,7 +91,6 @@ namespace osu.Game.Tests.Mods
|
||||
{
|
||||
public override string Name => "Non-matching setting type mod";
|
||||
public override LocalisableString Description => "Description";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override ModType Type => ModType.Conversion;
|
||||
|
||||
[SettingSource("Test setting")]
|
||||
|
||||
@@ -421,7 +421,6 @@ namespace osu.Game.Tests.Mods
|
||||
public override string Name => string.Empty;
|
||||
public override LocalisableString Description => string.Empty;
|
||||
public override string Acronym => string.Empty;
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool HasImplementation => true;
|
||||
public override bool ValidForMultiplayer => false;
|
||||
public override bool ValidForMultiplayerAsFreeMod => false;
|
||||
@@ -432,7 +431,6 @@ namespace osu.Game.Tests.Mods
|
||||
public override string Name => string.Empty;
|
||||
public override LocalisableString Description => string.Empty;
|
||||
public override string Acronym => string.Empty;
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool HasImplementation => true;
|
||||
public override bool ValidForMultiplayerAsFreeMod => false;
|
||||
}
|
||||
@@ -441,7 +439,6 @@ namespace osu.Game.Tests.Mods
|
||||
{
|
||||
public override string Name => string.Empty;
|
||||
public override LocalisableString Description => string.Empty;
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override string Acronym => string.Empty;
|
||||
public override bool HasImplementation => true;
|
||||
public override bool ValidForFreestyleAsRequiredMod => false;
|
||||
|
||||
@@ -59,8 +59,6 @@ namespace osu.Game.Tests.Mods
|
||||
|
||||
public abstract class TestModCustomisable : Mod, IApplicableMod
|
||||
{
|
||||
public override double ScoreMultiplier => 1.0;
|
||||
|
||||
public override LocalisableString Description => "This is a customisable test mod.";
|
||||
|
||||
public override ModType Type => ModType.Conversion;
|
||||
|
||||
@@ -161,7 +161,6 @@ namespace osu.Game.Tests.NonVisual
|
||||
public override string Name => nameof(ModA);
|
||||
public override string Acronym => nameof(ModA);
|
||||
public override LocalisableString Description => string.Empty;
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithA), typeof(ModIncompatibleWithAAndB) };
|
||||
}
|
||||
@@ -171,7 +170,6 @@ namespace osu.Game.Tests.NonVisual
|
||||
public override string Name => nameof(ModB);
|
||||
public override LocalisableString Description => string.Empty;
|
||||
public override string Acronym => nameof(ModB);
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithAAndB) };
|
||||
}
|
||||
@@ -181,7 +179,6 @@ namespace osu.Game.Tests.NonVisual
|
||||
public override string Name => nameof(ModC);
|
||||
public override string Acronym => nameof(ModC);
|
||||
public override LocalisableString Description => string.Empty;
|
||||
public override double ScoreMultiplier => 1;
|
||||
}
|
||||
|
||||
private class ModIncompatibleWithA : Mod
|
||||
@@ -189,7 +186,6 @@ namespace osu.Game.Tests.NonVisual
|
||||
public override string Name => $"Incompatible With {nameof(ModA)}";
|
||||
public override string Acronym => $"Incompatible With {nameof(ModA)}";
|
||||
public override LocalisableString Description => string.Empty;
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModA) };
|
||||
}
|
||||
@@ -208,7 +204,6 @@ namespace osu.Game.Tests.NonVisual
|
||||
public override string Name => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}";
|
||||
public override string Acronym => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}";
|
||||
public override LocalisableString Description => string.Empty;
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModA), typeof(ModB) };
|
||||
}
|
||||
|
||||
@@ -185,7 +185,6 @@ namespace osu.Game.Tests.Online
|
||||
public override string Name => "Test Mod";
|
||||
public override string Acronym => "TM";
|
||||
public override LocalisableString Description => "This is a test mod.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
[SettingSource("Test")]
|
||||
public BindableNumber<double> TestSetting { get; } = new BindableDouble
|
||||
@@ -202,7 +201,6 @@ namespace osu.Game.Tests.Online
|
||||
public override string Name => "Test Mod";
|
||||
public override string Acronym => "TMTR";
|
||||
public override LocalisableString Description => "This is a test mod.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
[SettingSource("Initial rate", "The starting speed of the track")]
|
||||
public override BindableNumber<double> InitialRate { get; } = new BindableDouble(1.5)
|
||||
|
||||
@@ -104,7 +104,6 @@ namespace osu.Game.Tests.Online
|
||||
public override string Name => "Test Mod";
|
||||
public override string Acronym => "TM";
|
||||
public override LocalisableString Description => "This is a test mod.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
[SettingSource("Test")]
|
||||
public BindableNumber<double> TestSetting { get; } = new BindableDouble
|
||||
@@ -121,7 +120,6 @@ namespace osu.Game.Tests.Online
|
||||
public override string Name => "Test Mod";
|
||||
public override string Acronym => "TMTR";
|
||||
public override LocalisableString Description => "This is a test mod.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
[SettingSource("Initial rate", "The starting speed of the track")]
|
||||
public override BindableNumber<double> InitialRate { get; } = new BindableDouble(1.5)
|
||||
@@ -148,7 +146,6 @@ namespace osu.Game.Tests.Online
|
||||
public override string Name => "Test Mod";
|
||||
public override string Acronym => "TM";
|
||||
public override LocalisableString Description => "This is a test mod.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
[SettingSource("Test")]
|
||||
public Bindable<TestEnum> TestSetting { get; } = new Bindable<TestEnum>();
|
||||
|
||||
@@ -222,12 +222,10 @@ namespace osu.Game.Tests.Resources
|
||||
|
||||
private class TestModHardRock : ModHardRock
|
||||
{
|
||||
public override double ScoreMultiplier => 1;
|
||||
}
|
||||
|
||||
private class TestModDoubleTime : ModDoubleTime
|
||||
{
|
||||
public override double ScoreMultiplier => 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Tests.Rulesets.Scoring
|
||||
{
|
||||
@@ -12,7 +14,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[Test]
|
||||
public void TestFlatMultiplier()
|
||||
{
|
||||
var calculator = new TestScoreMultiplierCalculator();
|
||||
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty()));
|
||||
|
||||
double multiplier = calculator.CalculateFor([new OsuModEasy()]);
|
||||
|
||||
@@ -22,7 +24,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[Test]
|
||||
public void TestSettingDependentMultiplier()
|
||||
{
|
||||
var calculator = new TestScoreMultiplierCalculator();
|
||||
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty()));
|
||||
|
||||
double multiplier = calculator.CalculateFor([new OsuModDaycore { SpeedChange = { Value = 0.6 } }]);
|
||||
|
||||
@@ -30,28 +32,47 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContextDependentMultiplier()
|
||||
public void TestScoreDependentMultiplier()
|
||||
{
|
||||
var calculator = new TestScoreMultiplierCalculator();
|
||||
TestScoreMultiplierCalculator calculator;
|
||||
|
||||
double multiplier;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
calculator.HardRockPenalty = false;
|
||||
calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty()));
|
||||
multiplier = calculator.CalculateFor([new OsuModHardRock()]);
|
||||
Assert.That(multiplier, Is.EqualTo(1.4));
|
||||
|
||||
calculator.HardRockPenalty = true;
|
||||
calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty(), new ScoreInfo { ClientVersion = "2024.123.0" }));
|
||||
multiplier = calculator.CalculateFor([new OsuModHardRock()]);
|
||||
Assert.That(multiplier, Is.EqualTo(1.2));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDifficultyDependentMultiplier()
|
||||
{
|
||||
TestScoreMultiplierCalculator calculator;
|
||||
|
||||
double multiplier;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty()));
|
||||
multiplier = calculator.CalculateFor([new OsuModEasy()]);
|
||||
Assert.That(multiplier, Is.EqualTo(0.15));
|
||||
|
||||
calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty { ApproachRate = 0 }));
|
||||
multiplier = calculator.CalculateFor([new OsuModEasy()]);
|
||||
Assert.That(multiplier, Is.EqualTo(0.1));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCombinationMultiplier()
|
||||
{
|
||||
var calculator = new TestScoreMultiplierCalculator();
|
||||
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty()));
|
||||
|
||||
double multiplier = calculator.CalculateFor([new OsuModEasy(), new OsuModDaycore()]);
|
||||
|
||||
@@ -61,7 +82,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[Test]
|
||||
public void TestCombinationAndFlatMultipliers()
|
||||
{
|
||||
var calculator = new TestScoreMultiplierCalculator();
|
||||
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new BeatmapDifficulty()));
|
||||
|
||||
double multiplier = calculator.CalculateFor([new OsuModDaycore(), new OsuModHardRock(), new OsuModEasy()]);
|
||||
|
||||
@@ -70,15 +91,14 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
|
||||
private class TestScoreMultiplierCalculator : ScoreMultiplierCalculator
|
||||
{
|
||||
static TestScoreMultiplierCalculator()
|
||||
public TestScoreMultiplierCalculator(ScoreMultiplierContext context)
|
||||
: base(context)
|
||||
{
|
||||
Single<OsuModEasy>(hasMultiplier: 0.15);
|
||||
Single<OsuModEasy>(hasMultiplier: context.BeatmapDifficultyWithoutMods.ApproachRate == 0 ? 0.1 : 0.15);
|
||||
Single<OsuModDaycore>(hasMultiplier: daycore => (1 + daycore.SpeedChange.Value) / 4);
|
||||
Single<OsuModHardRock, TestScoreMultiplierCalculator>(hasMultiplier: (_, ctx) => ctx.HardRockPenalty ? 1.2 : 1.4);
|
||||
Single<OsuModHardRock>(hasMultiplier: _ => context.Score?.ClientVersion == "2024.123.0" ? 1.2 : 1.4);
|
||||
Combination<OsuModEasy, OsuModDaycore>(hasMultiplier: (_, _) => 0.003);
|
||||
}
|
||||
|
||||
public bool HardRockPenalty { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,6 +481,29 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
Assert.That(scoreProcessor.HighestCombo.Value, Is.Zero);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScoreMultiplier()
|
||||
{
|
||||
Mod[] mods = new Mod[] { new OsuModHardRock() };
|
||||
|
||||
scoreProcessor = new TestScoreProcessor();
|
||||
scoreProcessor.Mods.Value = mods;
|
||||
|
||||
var workingBeatmap = new TestWorkingBeatmap(beatmap);
|
||||
var playableBeatmap = workingBeatmap.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, mods);
|
||||
|
||||
scoreProcessor.ApplyBeatmap(playableBeatmap);
|
||||
|
||||
var judgementResult = new JudgementResult(beatmap.HitObjects.Single(), new OsuJudgement())
|
||||
{
|
||||
Type = HitResult.Great,
|
||||
};
|
||||
scoreProcessor.ApplyResult(judgementResult);
|
||||
|
||||
Assert.That(scoreProcessor.MaximumTotalScore, Is.EqualTo(1_000_000 * 1.1).Within(0.5d));
|
||||
Assert.That(scoreProcessor.GetDisplayScore(ScoringMode.Standardised), Is.EqualTo(1_000_000 * 1.1).Within(0.5d));
|
||||
}
|
||||
|
||||
private class TestJudgement : Judgement
|
||||
{
|
||||
public override HitResult MaxResult { get; }
|
||||
@@ -537,9 +560,20 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException();
|
||||
|
||||
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new TestScoreMultiplierCalculator(context);
|
||||
|
||||
public override string Description => string.Empty;
|
||||
public override string ShortName => string.Empty;
|
||||
}
|
||||
|
||||
private class TestScoreMultiplierCalculator : ScoreMultiplierCalculator
|
||||
{
|
||||
public TestScoreMultiplierCalculator(ScoreMultiplierContext context)
|
||||
: base(context)
|
||||
{
|
||||
Single<OsuModHardRock>(hasMultiplier: context.BeatmapDifficultyWithoutMods.CircleSize == 4 ? 1.1 : 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[HitResult.Miss] = 0,
|
||||
[HitResult.Meh] = 0,
|
||||
[HitResult.Great] = 0
|
||||
}, new ScoreProcessorStatistics(), DateTimeOffset.Now);
|
||||
}, new ScoreProcessorStatistics(), DateTimeOffset.Now, [], 0, []);
|
||||
}
|
||||
|
||||
switch (RNG.Next(0, 3))
|
||||
|
||||
@@ -178,7 +178,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
LoadComponent(Overlay = new FreeModSelectOverlay
|
||||
{
|
||||
SelectedMods = { BindTarget = FreeMods }
|
||||
SelectedMods = { BindTarget = FreeMods },
|
||||
Ruleset = { BindTarget = Ruleset }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -321,13 +321,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
ClickButtonWhenEnabled<UserModSelectButton>();
|
||||
AddUntilStep("mod select shows unranked", () => this.ChildrenOfType<RankingInformationDisplay>().Single().Ranked.Value == false);
|
||||
AddAssert("score multiplier = 1.20", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
|
||||
AddAssert("score multiplier = 1.45", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.45).Within(0.01));
|
||||
|
||||
AddStep("select flashlight", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().ChildrenOfType<ModPanel>().Single(m => m.Mod is ModFlashlight).TriggerClick());
|
||||
AddAssert("score multiplier = 1.35", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.35).Within(0.01));
|
||||
AddAssert("score multiplier = 1.74", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.74).Within(0.01));
|
||||
|
||||
AddStep("change flashlight setting", () => ((OsuModFlashlight)this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().SelectedMods.Value.Single()).FollowDelay.Value = 1200);
|
||||
AddAssert("score multiplier = 1.20", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
|
||||
AddStep("change flashlight setting", () => ((OsuModFlashlight)this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().SelectedMods.Value.Single()).ComboBasedSize.Value = false);
|
||||
AddAssert("score multiplier = 1.51", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.51).Within(0.01));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -8,10 +8,12 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Utils;
|
||||
@@ -63,37 +65,90 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Test]
|
||||
public void TestIncrementMultiplier()
|
||||
{
|
||||
var ruleset = new OsuRuleset();
|
||||
var hiddenMod = new Mod[] { new OsuModHidden() };
|
||||
|
||||
AddStep("Set ruleset", () => footerButtonMods.Ruleset.Value = ruleset.RulesetInfo);
|
||||
|
||||
AddStep(@"Add Hidden", () => changeMods(hiddenMod));
|
||||
assertModsMultiplier(hiddenMod);
|
||||
assertModsMultiplier(1.04);
|
||||
|
||||
var hardRockMod = new Mod[] { new OsuModHardRock() };
|
||||
AddStep(@"Add HardRock", () => changeMods(hardRockMod));
|
||||
assertModsMultiplier(hardRockMod);
|
||||
assertModsMultiplier(1.09);
|
||||
|
||||
var doubleTimeMod = new Mod[] { new OsuModDoubleTime() };
|
||||
AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod));
|
||||
assertModsMultiplier(doubleTimeMod);
|
||||
assertModsMultiplier(1.23);
|
||||
|
||||
var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() };
|
||||
AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods));
|
||||
assertModsMultiplier(multipleIncrementMods);
|
||||
assertModsMultiplier(1.23 * 1.04 * 1.09);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDecrementMultiplier()
|
||||
{
|
||||
var ruleset = new OsuRuleset();
|
||||
var easyMod = new Mod[] { new OsuModEasy() };
|
||||
|
||||
AddStep("Set ruleset", () => footerButtonMods.Ruleset.Value = ruleset.RulesetInfo);
|
||||
|
||||
AddStep(@"Add Easy", () => changeMods(easyMod));
|
||||
assertModsMultiplier(easyMod);
|
||||
assertModsMultiplier(0.8);
|
||||
|
||||
var noFailMod = new Mod[] { new OsuModNoFail() };
|
||||
AddStep(@"Add NoFail", () => changeMods(noFailMod));
|
||||
assertModsMultiplier(noFailMod);
|
||||
assertModsMultiplier(0.5);
|
||||
|
||||
var multipleDecrementMods = new Mod[] { new OsuModEasy(), new OsuModNoFail() };
|
||||
AddStep(@"Add Multiple Mods", () => changeMods(multipleDecrementMods));
|
||||
assertModsMultiplier(multipleDecrementMods);
|
||||
assertModsMultiplier(0.8 * 0.5);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDifficultyAdjustMultiplier()
|
||||
{
|
||||
var ruleset = new OsuRuleset();
|
||||
var difficultyAdjustMod = new OsuModDifficultyAdjust();
|
||||
|
||||
AddStep("Set ruleset", () => footerButtonMods.Ruleset.Value = ruleset.RulesetInfo);
|
||||
|
||||
AddStep("Set beatmap", () =>
|
||||
{
|
||||
var beatmap = CreateWorkingBeatmap(ruleset.RulesetInfo);
|
||||
beatmap.BeatmapInfo.Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
ApproachRate = 3,
|
||||
OverallDifficulty = 5,
|
||||
CircleSize = 5,
|
||||
DrainRate = 6,
|
||||
};
|
||||
Beatmap.Value = beatmap;
|
||||
});
|
||||
|
||||
AddStep(@"Set Difficulty Adjust", () => changeMods([difficultyAdjustMod]));
|
||||
assertModsMultiplier(1);
|
||||
|
||||
AddStep("Adjust AR", () => difficultyAdjustMod.ApproachRate.Value = 3.3f);
|
||||
assertModsMultiplier(0.85);
|
||||
|
||||
AddStep("Adjust HP", () => difficultyAdjustMod.DrainRate.Value = 6.5f);
|
||||
assertModsMultiplier(0.6375);
|
||||
|
||||
AddStep("Change beatmap", () =>
|
||||
{
|
||||
var beatmap = CreateWorkingBeatmap(ruleset.RulesetInfo);
|
||||
beatmap.BeatmapInfo.Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
ApproachRate = 3.3f,
|
||||
OverallDifficulty = 8,
|
||||
CircleSize = 8,
|
||||
DrainRate = 6,
|
||||
};
|
||||
Beatmap.Value = beatmap;
|
||||
});
|
||||
assertModsMultiplier(0.75);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -105,12 +160,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType<FooterButtonMods.UnrankedBadge>().Single().Alpha == 0);
|
||||
}
|
||||
|
||||
private void changeMods(IReadOnlyList<Mod> mods) => footerButtonMods.Current.Value = mods;
|
||||
private void changeMods(IReadOnlyList<Mod> mods) => footerButtonMods.Mods.Value = mods;
|
||||
|
||||
private void assertModsMultiplier(IEnumerable<Mod> mods)
|
||||
private void assertModsMultiplier(double expectedMultiplier)
|
||||
{
|
||||
double multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
||||
string expectedValue = ModUtils.FormatScoreMultiplier(multiplier).ToString();
|
||||
string expectedValue = ModUtils.FormatScoreMultiplier(expectedMultiplier).ToString();
|
||||
|
||||
AddAssert($"Displayed multiplier is {expectedValue}", () => footerButtonMods.ChildrenOfType<OsuSpriteText>().First(t => t.Text.ToString().Contains('x')).Text.ToString(), () => Is.EqualTo(expectedValue));
|
||||
}
|
||||
|
||||
@@ -244,7 +244,6 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
|
||||
public override string Name => string.Empty;
|
||||
public override LocalisableString Description => string.Empty;
|
||||
public override double ScoreMultiplier => 1.0;
|
||||
public override string Acronym => "Test";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ using osu.Framework.Localisation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
@@ -25,6 +26,8 @@ using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Footer;
|
||||
@@ -122,7 +125,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddUntilStep("two panels active", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
|
||||
AddAssert("mod multiplier correct", () =>
|
||||
{
|
||||
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
|
||||
double multiplier = new OsuScoreMultiplierCalculatorV2(new ScoreMultiplierContext(new BeatmapDifficulty())).CalculateFor(SelectedMods.Value);
|
||||
return Precision.AlmostEquals(multiplier, this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
|
||||
});
|
||||
assertCustomisationToggleState(disabled: false, active: false);
|
||||
@@ -137,7 +140,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddUntilStep("two panels active", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
|
||||
AddAssert("mod multiplier correct", () =>
|
||||
{
|
||||
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
|
||||
double multiplier = new OsuScoreMultiplierCalculatorV2(new ScoreMultiplierContext(new BeatmapDifficulty())).CalculateFor(SelectedMods.Value);
|
||||
return Precision.AlmostEquals(multiplier, this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
|
||||
});
|
||||
assertCustomisationToggleState(disabled: false, active: false);
|
||||
@@ -896,7 +899,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddAssert("difficulty multiplier display shows correct value",
|
||||
() => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.1).Within(Precision.DOUBLE_EPSILON));
|
||||
() => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.2).Within(Precision.DOUBLE_EPSILON));
|
||||
|
||||
// this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation,
|
||||
// it is instrumental in the reproduction of the failure scenario that this test is supposed to cover.
|
||||
@@ -906,7 +909,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single()
|
||||
.ChildrenOfType<RevertToDefaultButton<double>>().Single().TriggerClick());
|
||||
AddUntilStep("difficulty multiplier display shows correct value",
|
||||
() => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON));
|
||||
() => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.55).Within(Precision.DOUBLE_EPSILON));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -1029,6 +1032,51 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("OsuModPerfect panel active", () => getPanelForMod(typeof(OsuModPerfect)).Active.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDifficultyAdjustModMultiplierIsCalculatedCorrectly()
|
||||
{
|
||||
createScreen();
|
||||
AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() });
|
||||
AddUntilStep("one panel active", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value), () => Is.EqualTo(1));
|
||||
AddAssert("mod multiplier is correct", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value,
|
||||
() => Is.EqualTo(1).Within(Precision.FLOAT_EPSILON));
|
||||
assertCustomisationToggleState(disabled: false, active: false);
|
||||
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
|
||||
|
||||
AddStep("modify DA", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value = 3.9f);
|
||||
AddAssert("mod multiplier is correct", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value,
|
||||
() => Is.EqualTo(0.95).Within(Precision.FLOAT_EPSILON));
|
||||
|
||||
AddStep("replace DA completely", () =>
|
||||
{
|
||||
SelectedMods.Value = new Mod[]
|
||||
{
|
||||
new OsuModDifficultyAdjust
|
||||
{
|
||||
ApproachRate = { Value = 6.8f },
|
||||
OverallDifficulty = { Value = 6.2f }
|
||||
}
|
||||
};
|
||||
});
|
||||
AddAssert("mod multiplier is correct", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value,
|
||||
() => Is.EqualTo(0.81).Within(Precision.FLOAT_EPSILON));
|
||||
|
||||
AddStep("change beatmap", () =>
|
||||
{
|
||||
var beatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||
beatmap.BeatmapInfo.Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
ApproachRate = 6,
|
||||
OverallDifficulty = 5,
|
||||
CircleSize = 5,
|
||||
DrainRate = 5,
|
||||
};
|
||||
modSelectOverlay.Beatmap.Value = beatmap;
|
||||
});
|
||||
AddAssert("mod multiplier is correct", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value,
|
||||
() => Is.EqualTo(0.24).Within(Precision.FLOAT_EPSILON));
|
||||
}
|
||||
|
||||
private void waitForColumnLoad() => AddUntilStep("all column content loaded", () =>
|
||||
modSelectOverlay.ChildrenOfType<ModColumn>().Any()
|
||||
&& modSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded)
|
||||
@@ -1087,6 +1135,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
State = { Value = Visibility.Visible },
|
||||
Beatmap = { Value = Beatmap.Value },
|
||||
SelectedMods = { BindTarget = SelectedMods },
|
||||
Ruleset = { BindTarget = Ruleset },
|
||||
ShowPresets = true,
|
||||
});
|
||||
}
|
||||
@@ -1143,7 +1192,6 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
public override string Name => "Unimplemented mod";
|
||||
public override string Acronym => "UM";
|
||||
public override LocalisableString Description => "A mod that is not implemented.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override ModType Type => ModType.Conversion;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,8 @@ namespace osu.Game.Database
|
||||
|
||||
using var storyboardStreamReader = new LineBufferedReader(storyboardStream);
|
||||
var beatmapStoryboard = new LegacyStoryboardDecoder().Decode(storyboardStreamReader);
|
||||
beatmapStoryboard.Beatmap = beatmapContent;
|
||||
beatmapStoryboard.BeatmapInfo = beatmapInfo;
|
||||
|
||||
MutateBeatmap(model, playableBeatmap);
|
||||
|
||||
|
||||
@@ -187,10 +187,12 @@ namespace osu.Game.Database
|
||||
break;
|
||||
}
|
||||
|
||||
double modMultiplier = 1;
|
||||
var ruleset = score.Ruleset.CreateInstance();
|
||||
|
||||
foreach (var mod in score.Mods)
|
||||
modMultiplier *= mod.ScoreMultiplier;
|
||||
Debug.Assert(score.BeatmapInfo != null);
|
||||
|
||||
var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(score.BeatmapInfo.Difficulty, score));
|
||||
double modMultiplier = scoreMultiplierCalculator.CalculateFor(score.Mods);
|
||||
|
||||
return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier);
|
||||
|
||||
@@ -352,7 +354,8 @@ namespace osu.Game.Database
|
||||
long maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore;
|
||||
double bonusProportion = Math.Max(0, ((long)score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio);
|
||||
|
||||
double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n);
|
||||
var modMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(difficulty));
|
||||
double modMultiplier = modMultiplierCalculator.CalculateFor(score.Mods);
|
||||
|
||||
long convertedTotalScoreWithoutMods;
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ namespace osu.Game.Graphics
|
||||
public static IconUsage EditorDistanceSnap => get(OsuIconMapping.EditorDistanceSnap);
|
||||
public static IconUsage EditorFinish => get(OsuIconMapping.EditorFinish);
|
||||
public static IconUsage EditorGridSnap => get(OsuIconMapping.EditorGridSnap);
|
||||
public static IconUsage EditorNewCombo => get(OsuIconMapping.EditorNewCombo);
|
||||
public static IconUsage EditorNewComboSparkles => get(OsuIconMapping.EditorNewComboSparkles);
|
||||
public static IconUsage EditorSelect => get(OsuIconMapping.EditorSelect);
|
||||
public static IconUsage EditorSound => get(OsuIconMapping.EditorSound);
|
||||
public static IconUsage EditorWhistle => get(OsuIconMapping.EditorWhistle);
|
||||
@@ -459,8 +459,8 @@ namespace osu.Game.Graphics
|
||||
[Description(@"Editor/grid-snap")]
|
||||
EditorGridSnap,
|
||||
|
||||
[Description(@"Editor/new-combo")]
|
||||
EditorNewCombo,
|
||||
[Description(@"Editor/new-combo-sparkles")]
|
||||
EditorNewComboSparkles,
|
||||
|
||||
[Description(@"Editor/select")]
|
||||
EditorSelect,
|
||||
|
||||
@@ -1011,6 +1011,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
APIRoom.AutoStartDuration = Room.Settings.AutoStartDuration;
|
||||
APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == settings.PlaylistItemId);
|
||||
APIRoom.AutoSkip = Room.Settings.AutoSkip;
|
||||
APIRoom.MaxParticipants = Room.Settings.MaxParticipants;
|
||||
|
||||
SettingsChanged?.Invoke(settings);
|
||||
RoomUpdated?.Invoke();
|
||||
|
||||
@@ -62,12 +62,33 @@ namespace osu.Game.Online.Spectator
|
||||
/// The set of mods currently active.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Nullable for backwards compatibility with older clients
|
||||
/// (these structures are also used server-side, and <see langword="null"/> will be used as marker that the data isn't there).
|
||||
/// can be made non-nullable 20250407
|
||||
/// This is sent to spectator as mods can change during a play - one relevant circumstance
|
||||
/// is the automatic activation of Touch Device mod when usage of touch devices is detected.
|
||||
/// </remarks>
|
||||
[Key(7)]
|
||||
public APIMod[]? Mods { get; set; }
|
||||
public APIMod[] Mods { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The current total score without mod multipliers active.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Nullable for backwards compatibility with older clients that don't send this
|
||||
/// (server-side <see langword="null"/> is used to distinguish the lack of this data).
|
||||
/// can be made non-nullable 20261126
|
||||
/// </remarks>
|
||||
[Key(8)]
|
||||
public long? TotalScoreWithoutMods { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of time instants in the play at which the player paused the game.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Nullable for backwards compatibility with older clients that don't send this
|
||||
/// (server-side <see langword="null"/> is used to distinguish the lack of this data).
|
||||
/// can be made non-nullable 20261126
|
||||
/// </remarks>
|
||||
[Key(9)]
|
||||
public int[]? Pauses { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Construct header summary information from a point-in-time reference to a score which is actively being played.
|
||||
@@ -83,13 +104,25 @@ namespace osu.Game.Online.Spectator
|
||||
// copy for safety
|
||||
Statistics = new Dictionary<HitResult, int>(score.Statistics);
|
||||
Mods = score.APIMods.ToArray();
|
||||
TotalScoreWithoutMods = score.TotalScoreWithoutMods;
|
||||
Pauses = score.Pauses.ToArray();
|
||||
|
||||
ScoreProcessorStatistics = statistics;
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
[SerializationConstructor]
|
||||
public FrameHeader(long totalScore, double accuracy, int combo, int maxCombo, Dictionary<HitResult, int> statistics, ScoreProcessorStatistics scoreProcessorStatistics, DateTimeOffset receivedTime)
|
||||
public FrameHeader(
|
||||
long totalScore,
|
||||
double accuracy,
|
||||
int combo,
|
||||
int maxCombo,
|
||||
Dictionary<HitResult, int> statistics,
|
||||
ScoreProcessorStatistics scoreProcessorStatistics,
|
||||
DateTimeOffset receivedTime,
|
||||
APIMod[] mods,
|
||||
long? totalScoreWithoutMods,
|
||||
int[]? pauses)
|
||||
{
|
||||
TotalScore = totalScore;
|
||||
Accuracy = accuracy;
|
||||
@@ -98,6 +131,9 @@ namespace osu.Game.Online.Spectator
|
||||
Statistics = statistics;
|
||||
ScoreProcessorStatistics = scoreProcessorStatistics;
|
||||
ReceivedTime = receivedTime;
|
||||
Mods = mods;
|
||||
TotalScoreWithoutMods = totalScoreWithoutMods;
|
||||
Pauses = pauses;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -1301,10 +1301,17 @@ namespace osu.Game
|
||||
|
||||
applyConfigMigrations();
|
||||
|
||||
string lastVersion = LocalConfig.Get<string>(OsuSetting.Version);
|
||||
string version = Version;
|
||||
|
||||
// only show a notification if we've previously saved a version to the config file (ie. not the first run).
|
||||
if (IsDeployedBuild && !string.IsNullOrEmpty(lastVersion) && version != lastVersion)
|
||||
Notifications.Post(new UpdateCompleteNotification(version));
|
||||
|
||||
// finally, update the version stored to the configuration.
|
||||
// this MUST happen after `applyConfigMigrations()` call, as it relies on comparing the previous version.
|
||||
// debug / local compilations will reset to a non-release string.
|
||||
LocalConfig.SetValue(OsuSetting.Version, Version);
|
||||
LocalConfig.SetValue(OsuSetting.Version, version);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -10,7 +10,9 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
@@ -28,6 +30,7 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
public readonly IBindable<WorkingBeatmap?> Beatmap = new Bindable<WorkingBeatmap?>();
|
||||
public readonly IBindable<IReadOnlyList<Mod>> ActiveMods = new Bindable<IReadOnlyList<Mod>>();
|
||||
public readonly IBindable<RulesetInfo?> Ruleset = new Bindable<RulesetInfo?>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the effects (on score multiplier, on or beatmap difficulty) of the current selected set of mods should be shown.
|
||||
@@ -99,8 +102,11 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
if (beatmapAttributesDisplay != null)
|
||||
beatmapAttributesDisplay.BeatmapInfo.Value = b.NewValue?.BeatmapInfo;
|
||||
|
||||
updateInformation();
|
||||
}, true);
|
||||
|
||||
Ruleset.BindValueChanged(_ => updateInformation());
|
||||
ActiveMods.BindValueChanged(m =>
|
||||
{
|
||||
updateInformation();
|
||||
@@ -118,12 +124,12 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
private void updateInformation()
|
||||
{
|
||||
if (rankingInformationDisplay != null)
|
||||
{
|
||||
double multiplier = 1.0;
|
||||
WorkingBeatmap? workingBeatmap = Beatmap.Value;
|
||||
|
||||
foreach (var mod in ActiveMods.Value)
|
||||
multiplier *= mod.ScoreMultiplier;
|
||||
if (rankingInformationDisplay != null && workingBeatmap != null)
|
||||
{
|
||||
var scoreMultiplierCalculator = Ruleset.Value?.CreateInstance().CreateScoreMultiplierCalculator(new ScoreMultiplierContext(workingBeatmap.BeatmapInfo.Difficulty));
|
||||
double multiplier = scoreMultiplierCalculator?.CalculateFor(ActiveMods.Value) ?? 1;
|
||||
|
||||
rankingInformationDisplay.ModMultiplier.Value = multiplier;
|
||||
rankingInformationDisplay.Ranked.Value = ActiveMods.Value.All(m => m.Ranked);
|
||||
|
||||
@@ -26,6 +26,7 @@ using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Footer;
|
||||
using osu.Game.Utils;
|
||||
@@ -121,6 +122,7 @@ namespace osu.Game.Overlays.Mods
|
||||
private Sample? columnAppearSample;
|
||||
|
||||
public readonly Bindable<WorkingBeatmap?> Beatmap = new Bindable<WorkingBeatmap?>();
|
||||
public readonly Bindable<RulesetInfo?> Ruleset = new Bindable<RulesetInfo?>();
|
||||
|
||||
[Resolved]
|
||||
private ScreenFooter? footer { get; set; }
|
||||
@@ -283,6 +285,7 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
Beatmap = { BindTarget = Beatmap },
|
||||
ActiveMods = { BindTarget = ActiveMods },
|
||||
Ruleset = { BindTarget = Ruleset },
|
||||
};
|
||||
|
||||
private static readonly LocalisableString input_search_placeholder = Resources.Localisation.Web.CommonStrings.InputSearch;
|
||||
|
||||
@@ -84,7 +84,8 @@ namespace osu.Game.Rulesets.Mods
|
||||
/// The score multiplier of this mod.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public abstract double ScoreMultiplier { get; }
|
||||
[Obsolete("This property is no longer used to calculate the score multiplier. Use `Ruleset.CreateScoreMultiplierCalculator()` instead.")]
|
||||
public virtual double ScoreMultiplier => 1;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this mod is implemented (and playable).
|
||||
|
||||
@@ -30,8 +30,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
|
||||
public override double ScoreMultiplier => 1.0;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModEasyWithExtraLives), typeof(ModPerfect) }).ToArray();
|
||||
|
||||
public override bool RequiresConfiguration => false;
|
||||
|
||||
@@ -33,8 +33,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override ModType Type => ModType.Fun;
|
||||
|
||||
public override double ScoreMultiplier => 0.5;
|
||||
|
||||
public sealed override bool ValidForMultiplayer => false;
|
||||
public sealed override bool ValidForMultiplayerAsFreeMod => false;
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModAutoplay;
|
||||
public override ModType Type => ModType.Automation;
|
||||
public override LocalisableString Description => "Watch a perfect automated play through the song.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public sealed override bool UserPlayable => false;
|
||||
public sealed override bool ValidForMultiplayer => false;
|
||||
|
||||
@@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override string Acronym => "BR";
|
||||
public override IconUsage? Icon => OsuIcon.ModBarrelRoll;
|
||||
public override LocalisableString Description => "The whole playfield is on a wheel!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
|
||||
@@ -13,8 +13,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override string Acronym => "CL";
|
||||
|
||||
public override double ScoreMultiplier => 0.96;
|
||||
|
||||
public override IconUsage? Icon => OsuIcon.ModClassic;
|
||||
|
||||
public override LocalisableString Description => "Feeling nostalgic?";
|
||||
|
||||
@@ -30,13 +30,10 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
private readonly BindableNumber<double> tempoAdjust = new BindableDouble(1);
|
||||
private readonly BindableNumber<double> freqAdjust = new BindableDouble(1);
|
||||
private readonly RateAdjustModHelper rateAdjustHelper;
|
||||
|
||||
protected ModDaycore()
|
||||
{
|
||||
rateAdjustHelper = new RateAdjustModHelper(SpeedChange);
|
||||
|
||||
// intentionally not deferring the speed change handling to `RateAdjustModHelper`
|
||||
// intentionally not using `RateAdjustModHelper`
|
||||
// as the expected result of operation is not the same (daycore should preserve constant pitch).
|
||||
SpeedChange.BindValueChanged(val =>
|
||||
{
|
||||
@@ -50,7 +47,5 @@ namespace osu.Game.Rulesets.Mods
|
||||
track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust);
|
||||
track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust);
|
||||
}
|
||||
|
||||
public override double ScoreMultiplier => rateAdjustHelper.ScoreMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override IconUsage? Icon => OsuIcon.ModDifficultyAdjust;
|
||||
|
||||
public override double ScoreMultiplier => 0.5;
|
||||
|
||||
public override bool RequiresConfiguration => true;
|
||||
|
||||
public override bool ValidForFreestyleAsRequiredMod => true;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user