mirror of
https://github.com/ppy/osu.git
synced 2026-05-27 04:19:54 +08:00
Compare commits
9 Commits
2026.522.0
...
2026.526.0
@@ -42,29 +42,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());
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Tests.Rulesets;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
@@ -14,28 +15,141 @@ 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() }, 0.96],
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
{
|
||||
public class CatchScoreMultiplierCalculator : ScoreMultiplierCalculator
|
||||
{
|
||||
static CatchScoreMultiplierCalculator()
|
||||
public CatchScoreMultiplierCalculator(ScoreMultiplierContext context)
|
||||
: base(context)
|
||||
{
|
||||
#region Difficulty Reduction
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
// 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.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 +19,205 @@ 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() }, 0.96],
|
||||
[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 ScoreInfo
|
||||
{
|
||||
Date = endDate,
|
||||
ClientVersion = clientVersion
|
||||
}));
|
||||
Assert.That(calculator.CalculateFor([new ManiaModKey4()]), 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()).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";
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -46,16 +49,16 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
// 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,49 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Tests.Rulesets;
|
||||
|
||||
@@ -14,32 +15,165 @@ 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
|
||||
|
||||
[Test]
|
||||
public void TestHiddenOnNonDefaultSettings()
|
||||
=> TestModCombination([new OsuModHidden { OnlyFadeApproachCircles = { Value = true } }]);
|
||||
[new Mod[] { new OsuModEasy() }, 0.5],
|
||||
[new Mod[] { new OsuModNoFail() }, 0.5],
|
||||
|
||||
[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 } }]);
|
||||
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.50 } } }, 0.1],
|
||||
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.55 } } }, 0.1],
|
||||
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.60 } } }, 0.2],
|
||||
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.65 } } }, 0.2],
|
||||
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.70 } } }, 0.3],
|
||||
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.75 } } }, 0.3],
|
||||
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.80 } } }, 0.4],
|
||||
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.85 } } }, 0.4],
|
||||
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.90 } } }, 0.5],
|
||||
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.95 } } }, 0.5],
|
||||
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.99 } } }, 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 OsuModDaycore { SpeedChange = { Value = speedChange } }]);
|
||||
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.50 } } }, 0.1],
|
||||
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.55 } } }, 0.1],
|
||||
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.60 } } }, 0.2],
|
||||
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.65 } } }, 0.2],
|
||||
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.70 } } }, 0.3],
|
||||
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.75 } } }, 0.3],
|
||||
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.80 } } }, 0.4],
|
||||
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.85 } } }, 0.4],
|
||||
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.90 } } }, 0.5],
|
||||
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.95 } } }, 0.5],
|
||||
[new Mod[] { new OsuModDaycore { 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 OsuModDoubleTime { SpeedChange = { Value = speedChange } }]);
|
||||
#endregion
|
||||
|
||||
[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 } }]);
|
||||
#region Difficulty Increase
|
||||
|
||||
[Test]
|
||||
public void TestMultiplicativeCombination()
|
||||
=> TestModCombination([new OsuModHidden(), new OsuModHardRock()]);
|
||||
[new Mod[] { new OsuModHardRock() }, 1.06],
|
||||
[new Mod[] { new OsuModSuddenDeath() }, 1],
|
||||
[new Mod[] { new OsuModPerfect() }, 1],
|
||||
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } }, 1.00],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.05 } } }, 1.00],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.10 } } }, 1.02],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.15 } } }, 1.02],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.20 } } }, 1.04],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } }, 1.04],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.30 } } }, 1.06],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.35 } } }, 1.06],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.40 } } }, 1.08],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.45 } } }, 1.08],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.50 } } }, 1.10],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.55 } } }, 1.10],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.60 } } }, 1.12],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.65 } } }, 1.12],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.70 } } }, 1.14],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.75 } } }, 1.14],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.80 } } }, 1.16],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.85 } } }, 1.16],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.90 } } }, 1.18],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.95 } } }, 1.18],
|
||||
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2.00 } } }, 1.20],
|
||||
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.01 } } }, 1.00],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.05 } } }, 1.00],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.10 } } }, 1.02],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.15 } } }, 1.02],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.20 } } }, 1.04],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.25 } } }, 1.04],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.30 } } }, 1.06],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.35 } } }, 1.06],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.40 } } }, 1.08],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.45 } } }, 1.08],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.50 } } }, 1.10],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.55 } } }, 1.10],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.60 } } }, 1.12],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.65 } } }, 1.12],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.70 } } }, 1.14],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.75 } } }, 1.14],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.80 } } }, 1.16],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.85 } } }, 1.16],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.90 } } }, 1.18],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.95 } } }, 1.18],
|
||||
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 2.00 } } }, 1.20],
|
||||
|
||||
[new Mod[] { new OsuModHidden() }, 1.06],
|
||||
[new Mod[] { new OsuModHidden { OnlyFadeApproachCircles = { Value = true } } }, 1],
|
||||
|
||||
[new Mod[] { new OsuModTraceable() }, 1],
|
||||
|
||||
[new Mod[] { new OsuModFlashlight() }, 1.12],
|
||||
[new Mod[] { new OsuModFlashlight { ComboBasedSize = { Value = false } } }, 1],
|
||||
|
||||
[new Mod[] { new OsuModBlinds() }, 1.12],
|
||||
[new Mod[] { new OsuModStrictTracking() }, 1],
|
||||
[new Mod[] { new OsuModAccuracyChallenge() }, 1],
|
||||
|
||||
#endregion
|
||||
|
||||
#region Conversion
|
||||
|
||||
[new Mod[] { new OsuModTargetPractice() }, 0.1],
|
||||
[new Mod[] { new OsuModDifficultyAdjust() }, 0.5],
|
||||
[new Mod[] { new OsuModClassic() }, 0.96],
|
||||
[new Mod[] { new OsuModRandom() }, 1],
|
||||
[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.9],
|
||||
|
||||
#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 ModWindUp() }, 0.5],
|
||||
[new Mod[] { new ModWindDown() }, 0.5],
|
||||
[new Mod[] { new OsuModBarrelRoll() }, 1],
|
||||
[new Mod[] { new OsuModApproachDifferent() }, 1],
|
||||
[new Mod[] { new OsuModMuted() }, 1],
|
||||
[new Mod[] { new OsuModNoScope() }, 1],
|
||||
[new Mod[] { new OsuModMagnetised() }, 0.5],
|
||||
[new Mod[] { new OsuModRepel() }, 1],
|
||||
[new Mod[] { new ModAdaptiveSpeed() }, 0.5],
|
||||
[new Mod[] { new OsuModFreezeFrame() }, 1],
|
||||
[new Mod[] { new OsuModBubbles() }, 1],
|
||||
[new Mod[] { new OsuModSynesthesia() }, 0.8],
|
||||
[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.06 * 1.06],
|
||||
|
||||
#endregion
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestMultipliers(Mod[] mods, double expectedMultiplier)
|
||||
=> TestModCombination(mods, expectedMultiplier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
}
|
||||
}
|
||||
|
||||
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new OsuScoreMultiplierCalculator();
|
||||
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new OsuScoreMultiplierCalculator(context);
|
||||
|
||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetOsu };
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
||||
{
|
||||
public class OsuScoreMultiplierCalculator : ScoreMultiplierCalculator
|
||||
{
|
||||
static OsuScoreMultiplierCalculator()
|
||||
public OsuScoreMultiplierCalculator(ScoreMultiplierContext context)
|
||||
: base(context)
|
||||
{
|
||||
#region Difficulty Reduction
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Tests.Rulesets;
|
||||
|
||||
@@ -14,28 +15,143 @@ 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() }, 0.96],
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
{
|
||||
public class TaikoScoreMultiplierCalculator : ScoreMultiplierCalculator
|
||||
{
|
||||
static TaikoScoreMultiplierCalculator()
|
||||
public TaikoScoreMultiplierCalculator(ScoreMultiplierContext context)
|
||||
: base(context)
|
||||
{
|
||||
#region Difficulty Reduction
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Tests.Rulesets.Scoring
|
||||
{
|
||||
@@ -12,7 +13,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[Test]
|
||||
public void TestFlatMultiplier()
|
||||
{
|
||||
var calculator = new TestScoreMultiplierCalculator();
|
||||
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext());
|
||||
|
||||
double multiplier = calculator.CalculateFor([new OsuModEasy()]);
|
||||
|
||||
@@ -22,7 +23,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[Test]
|
||||
public void TestSettingDependentMultiplier()
|
||||
{
|
||||
var calculator = new TestScoreMultiplierCalculator();
|
||||
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext());
|
||||
|
||||
double multiplier = calculator.CalculateFor([new OsuModDaycore { SpeedChange = { Value = 0.6 } }]);
|
||||
|
||||
@@ -32,17 +33,17 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[Test]
|
||||
public void TestContextDependentMultiplier()
|
||||
{
|
||||
var calculator = new TestScoreMultiplierCalculator();
|
||||
TestScoreMultiplierCalculator calculator;
|
||||
|
||||
double multiplier;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
calculator.HardRockPenalty = false;
|
||||
calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext());
|
||||
multiplier = calculator.CalculateFor([new OsuModHardRock()]);
|
||||
Assert.That(multiplier, Is.EqualTo(1.4));
|
||||
|
||||
calculator.HardRockPenalty = true;
|
||||
calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new ScoreInfo { ClientVersion = "2024.123.0" }));
|
||||
multiplier = calculator.CalculateFor([new OsuModHardRock()]);
|
||||
Assert.That(multiplier, Is.EqualTo(1.2));
|
||||
});
|
||||
@@ -51,7 +52,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[Test]
|
||||
public void TestCombinationMultiplier()
|
||||
{
|
||||
var calculator = new TestScoreMultiplierCalculator();
|
||||
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext());
|
||||
|
||||
double multiplier = calculator.CalculateFor([new OsuModEasy(), new OsuModDaycore()]);
|
||||
|
||||
@@ -61,7 +62,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[Test]
|
||||
public void TestCombinationAndFlatMultipliers()
|
||||
{
|
||||
var calculator = new TestScoreMultiplierCalculator();
|
||||
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext());
|
||||
|
||||
double multiplier = calculator.CalculateFor([new OsuModDaycore(), new OsuModHardRock(), new OsuModEasy()]);
|
||||
|
||||
@@ -70,15 +71,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<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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,11 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Utils;
|
||||
|
||||
@@ -63,37 +66,45 @@ 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(ruleset, hiddenMod);
|
||||
|
||||
var hardRockMod = new Mod[] { new OsuModHardRock() };
|
||||
AddStep(@"Add HardRock", () => changeMods(hardRockMod));
|
||||
assertModsMultiplier(hardRockMod);
|
||||
assertModsMultiplier(ruleset, hardRockMod);
|
||||
|
||||
var doubleTimeMod = new Mod[] { new OsuModDoubleTime() };
|
||||
AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod));
|
||||
assertModsMultiplier(doubleTimeMod);
|
||||
assertModsMultiplier(ruleset, doubleTimeMod);
|
||||
|
||||
var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() };
|
||||
AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods));
|
||||
assertModsMultiplier(multipleIncrementMods);
|
||||
assertModsMultiplier(ruleset, multipleIncrementMods);
|
||||
}
|
||||
|
||||
[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(ruleset, easyMod);
|
||||
|
||||
var noFailMod = new Mod[] { new OsuModNoFail() };
|
||||
AddStep(@"Add NoFail", () => changeMods(noFailMod));
|
||||
assertModsMultiplier(noFailMod);
|
||||
assertModsMultiplier(ruleset, noFailMod);
|
||||
|
||||
var multipleDecrementMods = new Mod[] { new OsuModEasy(), new OsuModNoFail() };
|
||||
AddStep(@"Add Multiple Mods", () => changeMods(multipleDecrementMods));
|
||||
assertModsMultiplier(multipleDecrementMods);
|
||||
assertModsMultiplier(ruleset, multipleDecrementMods);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -105,11 +116,12 @@ 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(Ruleset ruleset, IEnumerable<Mod> mods)
|
||||
{
|
||||
double multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
||||
var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
|
||||
double multiplier = scoreMultiplierCalculator.CalculateFor(mods);
|
||||
string expectedValue = ModUtils.FormatScoreMultiplier(multiplier).ToString();
|
||||
|
||||
AddAssert($"Displayed multiplier is {expectedValue}", () => footerButtonMods.ChildrenOfType<OsuSpriteText>().First(t => t.Text.ToString().Contains('x')).Text.ToString(), () => Is.EqualTo(expectedValue));
|
||||
|
||||
@@ -25,6 +25,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 +124,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 OsuScoreMultiplierCalculator(new ScoreMultiplierContext()).CalculateFor(SelectedMods.Value);
|
||||
return Precision.AlmostEquals(multiplier, this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
|
||||
});
|
||||
assertCustomisationToggleState(disabled: false, active: false);
|
||||
@@ -137,7 +139,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 OsuScoreMultiplierCalculator(new ScoreMultiplierContext()).CalculateFor(SelectedMods.Value);
|
||||
return Precision.AlmostEquals(multiplier, this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
|
||||
});
|
||||
assertCustomisationToggleState(disabled: false, active: false);
|
||||
@@ -1087,6 +1089,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
State = { Value = Visibility.Visible },
|
||||
Beatmap = { Value = Beatmap.Value },
|
||||
SelectedMods = { BindTarget = SelectedMods },
|
||||
Ruleset = { BindTarget = Ruleset },
|
||||
ShowPresets = true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,9 @@ namespace osu.Game.Database
|
||||
break;
|
||||
}
|
||||
|
||||
double modMultiplier = 1;
|
||||
|
||||
foreach (var mod in score.Mods)
|
||||
modMultiplier *= mod.ScoreMultiplier;
|
||||
var ruleset = score.Ruleset.CreateInstance();
|
||||
var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(score));
|
||||
double modMultiplier = scoreMultiplierCalculator.CalculateFor(score.Mods);
|
||||
|
||||
return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier);
|
||||
|
||||
@@ -352,7 +351,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());
|
||||
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.
|
||||
@@ -101,6 +104,7 @@ namespace osu.Game.Overlays.Mods
|
||||
beatmapAttributesDisplay.BeatmapInfo.Value = b.NewValue?.BeatmapInfo;
|
||||
}, true);
|
||||
|
||||
Ruleset.BindValueChanged(_ => updateInformation());
|
||||
ActiveMods.BindValueChanged(m =>
|
||||
{
|
||||
updateInformation();
|
||||
@@ -120,10 +124,8 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
if (rankingInformationDisplay != null)
|
||||
{
|
||||
double multiplier = 1.0;
|
||||
|
||||
foreach (var mod in ActiveMods.Value)
|
||||
multiplier *= mod.ScoreMultiplier;
|
||||
var scoreMultiplierCalculator = Ruleset.Value?.CreateInstance().CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
|
||||
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;
|
||||
|
||||
@@ -213,7 +213,7 @@ namespace osu.Game.Rulesets
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ScoreMultiplierCalculator"/> relevant to this ruleset.
|
||||
/// </summary>
|
||||
public virtual ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new ScoreMultiplierCalculator();
|
||||
public virtual ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new ScoreMultiplierCalculator(context);
|
||||
|
||||
/// <summary>
|
||||
/// Create a transformer which adds lookups specific to a ruleset to skin sources.
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
@@ -13,47 +14,42 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public class ScoreMultiplierCalculator
|
||||
{
|
||||
private static readonly List<(Type[] mods, Func<Mod[], double> multiplier)> combination_multipliers = [];
|
||||
private static readonly Dictionary<Type, Func<Mod, ScoreMultiplierCalculator, double>> single_multipliers_with_context = [];
|
||||
private static readonly Dictionary<Type, Func<Mod, double>> single_multipliers = [];
|
||||
protected ScoreMultiplierContext Context { get; }
|
||||
|
||||
private readonly List<(Type[] mods, Func<Mod[], double> multiplier)> combinationMultipliers = [];
|
||||
private readonly Dictionary<Type, Func<Mod, double>> singleMultipliers = [];
|
||||
|
||||
public ScoreMultiplierCalculator(ScoreMultiplierContext context)
|
||||
{
|
||||
Context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a flat, setting-independent score multiplier for the given <typeparamref name="TMod"/>.
|
||||
/// </summary>
|
||||
public static void Single<TMod>(double hasMultiplier)
|
||||
protected void Single<TMod>(double hasMultiplier)
|
||||
where TMod : Mod
|
||||
{
|
||||
single_multipliers[typeof(TMod)] = _ => hasMultiplier;
|
||||
singleMultipliers[typeof(TMod)] = _ => hasMultiplier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a setting-dependent score multiplier for the given <typeparamref name="TMod"/>.
|
||||
/// </summary>
|
||||
public static void Single<TMod>(Func<TMod, double> hasMultiplier)
|
||||
protected void Single<TMod>(Func<TMod, double> hasMultiplier)
|
||||
where TMod : Mod
|
||||
{
|
||||
single_multipliers[typeof(TMod)] = mod => hasMultiplier.Invoke((TMod)mod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a setting-dependent score multiplier for the given <typeparamref name="TMod"/>.
|
||||
/// The multiplier calculation is given additional context to calculate the multiplier via the <typeparamref name="TContext"/> type instance.
|
||||
/// </summary>
|
||||
public static void Single<TMod, TContext>(Func<TMod, TContext, double> hasMultiplier)
|
||||
where TMod : Mod
|
||||
where TContext : ScoreMultiplierCalculator
|
||||
{
|
||||
single_multipliers_with_context[typeof(TMod)] = (mod, context) => hasMultiplier.Invoke((TMod)mod, (TContext)context);
|
||||
singleMultipliers[typeof(TMod)] = mod => hasMultiplier.Invoke((TMod)mod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a score multiplier specific to when both <typeparamref name="T1"/> and <typeparamref name="T2"/> mods are present.
|
||||
/// </summary>
|
||||
public static void Combination<T1, T2>(Func<T1, T2, double> hasMultiplier)
|
||||
protected void Combination<T1, T2>(Func<T1, T2, double> hasMultiplier)
|
||||
where T1 : Mod
|
||||
where T2 : Mod
|
||||
{
|
||||
combination_multipliers.Add(([typeof(T1), typeof(T2)], mods => hasMultiplier((T1)mods[0], (T2)mods[1])));
|
||||
combinationMultipliers.Add(([typeof(T1), typeof(T2)], mods => hasMultiplier((T1)mods[0], (T2)mods[1])));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -72,7 +68,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
if (allModsByType.Count > 1)
|
||||
{
|
||||
foreach (var (combination, multiplier) in combination_multipliers)
|
||||
foreach (var (combination, multiplier) in combinationMultipliers)
|
||||
{
|
||||
if (remainingModTypes.IsSupersetOf(combination))
|
||||
{
|
||||
@@ -85,13 +81,48 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
foreach (var modType in remainingModTypes)
|
||||
{
|
||||
if (single_multipliers.TryGetValue(modType, out var multiplier))
|
||||
if (singleMultipliers.TryGetValue(modType, out var multiplier))
|
||||
result *= multiplier(allModsByType[modType]);
|
||||
else if (single_multipliers_with_context.TryGetValue(modType, out var multiplierWithContext))
|
||||
result *= multiplierWithContext(allModsByType[modType], this);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contextual information to pass to a <see cref="ScoreMultiplierContext"/>
|
||||
/// in order for it to calculate the correct multiplier.
|
||||
/// </summary>
|
||||
public class ScoreMultiplierContext
|
||||
{
|
||||
/// <summary>
|
||||
/// The score that the multipliers are calculated for.
|
||||
/// Mostly relevant and present in backwards compatibility scenarios.
|
||||
/// In usages where the current valid score multipliers are required, pass <see langword="null"/> or use a constructor that does not require this.
|
||||
/// </summary>
|
||||
public ScoreInfo? Score { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance.
|
||||
/// Use this in situations wherein the current valid score multipliers are needed.
|
||||
/// </summary>
|
||||
public ScoreMultiplierContext()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance.
|
||||
/// Use this in backwards compatibility scenarios when dealing with a specific <paramref name="score"/>.
|
||||
/// </summary>
|
||||
/// <param name="score">
|
||||
/// The score that the multipliers are calculated for.
|
||||
/// Mostly relevant and present in backwards compatibility scenarios.
|
||||
/// In usages where the current valid score multipliers are required, pass <see langword="null"/> or use a constructor that does not require this.
|
||||
/// </param>
|
||||
public ScoreMultiplierContext(ScoreInfo? score)
|
||||
{
|
||||
Score = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,10 +206,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
Mods.ValueChanged += mods =>
|
||||
{
|
||||
scoreMultiplier = 1;
|
||||
|
||||
foreach (var m in mods.NewValue)
|
||||
scoreMultiplier *= m.ScoreMultiplier;
|
||||
var calculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
|
||||
scoreMultiplier = calculator.CalculateFor(mods.NewValue);
|
||||
|
||||
updateScore();
|
||||
updateRank();
|
||||
|
||||
@@ -258,10 +258,9 @@ namespace osu.Game.Scoring.Legacy
|
||||
|
||||
public static void PopulateTotalScoreWithoutMods(ScoreInfo score)
|
||||
{
|
||||
double modMultiplier = 1;
|
||||
|
||||
foreach (var mod in score.Mods)
|
||||
modMultiplier *= mod.ScoreMultiplier;
|
||||
var ruleset = score.Ruleset.CreateInstance();
|
||||
var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(score));
|
||||
double modMultiplier = scoreMultiplierCalculator.CalculateFor(score.Mods);
|
||||
|
||||
score.TotalScoreWithoutMods = (long)Math.Round(score.TotalScore / modMultiplier);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO.Legacy;
|
||||
using osu.Game.IO.Serialization;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Replays.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
@@ -20,6 +21,19 @@ using SharpCompress.Compressors.LZMA;
|
||||
|
||||
namespace osu.Game.Scoring.Legacy
|
||||
{
|
||||
/// <summary>
|
||||
/// Encodes replays.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>When making <i>ANY</i> changes to the replay format to add new data, consider if:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="LATEST_VERSION"/> should be bumped accordingly,</item>
|
||||
/// <item>
|
||||
/// changes need to be made to <see cref="SpectatorClient"/> so that spectator server receives the new data being stored,
|
||||
/// as <b><i>spectator server</i> is responsible for the content of server-stored replays, <i>NOT</i> the client</b>.
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public class LegacyScoreEncoder
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
|
||||
{
|
||||
public partial class NewComboTernaryButton : CompositeDrawable, IHasCurrentValue<TernaryState>
|
||||
{
|
||||
public Func<Drawable>? CreateIcon { get; init; }
|
||||
|
||||
public Bindable<TernaryState> Current
|
||||
{
|
||||
get => current.Current;
|
||||
@@ -61,7 +63,7 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
|
||||
{
|
||||
Current = Current,
|
||||
Description = "New combo",
|
||||
CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorNewCombo },
|
||||
CreateIcon = CreateIcon,
|
||||
},
|
||||
},
|
||||
pickerButton = new ColourPickerButton
|
||||
|
||||
@@ -181,13 +181,50 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
public SampleBankTernaryButton[] SampleBankTernaryStates { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create the new combo ternary button. Mainly used to customize the displayed icon
|
||||
/// depending on the ruleset. Can be overriden to return null if a ruleset does not
|
||||
/// provide combo-supporting HitObjects.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[CanBeNull]
|
||||
protected virtual Drawable CreateNewComboButton() => new NewComboTernaryButton
|
||||
{
|
||||
Current = NewCombo,
|
||||
CreateIcon = () => new Container
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
// This is currently using the osu! hitcircle icon as a default in order
|
||||
// not to break any custom rulesets that depend on there being a defined
|
||||
// new combo button.
|
||||
// Could consider removing it and let rulesets specify their own buttons/icons.
|
||||
Icon = OsuIcon.EditorHitCircle,
|
||||
Size = new Vector2(15),
|
||||
},
|
||||
new SpriteIcon
|
||||
{
|
||||
Icon = OsuIcon.EditorNewComboSparkles,
|
||||
Size = new Vector2(20),
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create all ternary states required to be displayed to the user.
|
||||
/// </summary>
|
||||
protected virtual IEnumerable<Drawable> CreateTernaryButtons()
|
||||
{
|
||||
//TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects.
|
||||
yield return new NewComboTernaryButton { Current = NewCombo };
|
||||
var newComboButton = CreateNewComboButton();
|
||||
|
||||
if (newComboButton != null)
|
||||
yield return newComboButton;
|
||||
|
||||
foreach (var kvp in SelectionHandler.SelectionSampleStates)
|
||||
{
|
||||
|
||||
@@ -315,6 +315,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
{
|
||||
Beatmap = { BindTarget = Beatmap },
|
||||
SelectedMods = { BindTarget = userMods },
|
||||
Ruleset = { BindTarget = Ruleset },
|
||||
IsValidMod = _ => false
|
||||
});
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
{
|
||||
Beatmap = { BindTarget = Beatmap },
|
||||
ActiveMods = { BindTarget = ActiveMods },
|
||||
Ruleset = { BindTarget = Ruleset },
|
||||
};
|
||||
|
||||
public partial class FreeModSelectFooterContent : ModSelectFooterContent
|
||||
|
||||
@@ -77,6 +77,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
private DrawableSample resultsAppearSample = null!;
|
||||
private DrawableSample dmgFlySample = null!;
|
||||
private DrawableSample dmgHitSample = null!;
|
||||
private DrawableSample damageMultiplierSample = null!;
|
||||
private DrawableSample damageMultiplierUpSample = null!;
|
||||
private DrawableSample damageMultiplierDownSample = null!;
|
||||
private DrawableSample hpDownSample = null!;
|
||||
private DrawableSample playerAppearSample = null!;
|
||||
private DrawableSample pseudoScoreCounterSample = null!;
|
||||
@@ -337,6 +340,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
resultsAppearSample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/results-appear")),
|
||||
dmgFlySample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-fly")),
|
||||
dmgHitSample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-hit")),
|
||||
damageMultiplierSample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-multiplier")),
|
||||
damageMultiplierUpSample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-multiplier-up")),
|
||||
damageMultiplierDownSample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-multiplier-down")),
|
||||
hpDownSample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/hp-down")),
|
||||
playerAppearSample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/players-appear")),
|
||||
pseudoScoreCounterSample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/pseudo-score-counter")),
|
||||
@@ -515,14 +521,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
));
|
||||
}
|
||||
|
||||
int pitchChangeAmount = 0;
|
||||
|
||||
foreach (var breakdown in damageBreakdowns)
|
||||
{
|
||||
using (BeginDelayedSequence(delay))
|
||||
{
|
||||
int pitch = pitchChangeAmount;
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
damageBreakdownValueText.Text = breakdown.displayValue;
|
||||
damageBreakdownSourceText.Text = breakdown.source;
|
||||
|
||||
SampleChannel damageBreakdownChannel = damageMultiplierSample.GetChannel();
|
||||
damageBreakdownChannel.Frequency.Value = 1f + (pitch * .1f);
|
||||
damageBreakdownChannel.Play();
|
||||
});
|
||||
|
||||
damageBreakdownContainer.MoveToX(120)
|
||||
@@ -540,7 +554,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
|
||||
.ScaleTo(new Vector2(1.25f), 200, Easing.OutQuint)
|
||||
.Then()
|
||||
.ScaleTo(Vector2.One, 200);
|
||||
|
||||
SampleChannel scoreChangeChannel = breakdown.rawDamage > 0 ? damageMultiplierUpSample.GetChannel() : damageMultiplierDownSample.GetChannel();
|
||||
scoreChangeChannel.Frequency.Value = 1f + (pitch * .1f);
|
||||
scoreChangeChannel.Play();
|
||||
});
|
||||
|
||||
if (breakdown.rawDamage > 0)
|
||||
pitchChangeAmount++;
|
||||
else if (breakdown.rawDamage < 0)
|
||||
pitchChangeAmount--;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -291,7 +291,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
|
||||
protected override ModSelectOverlay CreateModSelectOverlay() => modSelect = new UserModSelectOverlay(OverlayColourScheme.Plum)
|
||||
{
|
||||
IsValidMod = isValidRequiredMod
|
||||
IsValidMod = isValidRequiredMod,
|
||||
};
|
||||
|
||||
public override IReadOnlyList<ScreenFooterButton> CreateFooterButtons()
|
||||
|
||||
@@ -421,7 +421,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
|
||||
LoadComponent(userModsSelectOverlay = new MultiplayerUserModSelectOverlay
|
||||
{
|
||||
Beatmap = { BindTarget = Beatmap }
|
||||
Beatmap = { BindTarget = Beatmap },
|
||||
Ruleset = { BindTarget = Ruleset },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
|
||||
for (byte i = 0; i < slotUserIds.Length; ++i)
|
||||
{
|
||||
var participant = slotUserIds[i] == null ? Slot.Empty(i) : Slot.FromUser(client.Room.Users.Single(u => u.UserID == slotUserIds[i]));
|
||||
var user = slotUserIds[i] != null ? client.Room.Users.SingleOrDefault(u => u.UserID == slotUserIds[i]) : null;
|
||||
var participant = user == null ? Slot.Empty(i) : Slot.FromUser(client.Room.Users.Single(u => u.UserID == slotUserIds[i]));
|
||||
|
||||
if (i >= slots.Count)
|
||||
slots.Add(participant);
|
||||
|
||||
@@ -445,6 +445,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
SelectedItem = { BindTarget = SelectedItem },
|
||||
SelectedMods = { BindTarget = UserMods },
|
||||
Beatmap = { BindTarget = Beatmap },
|
||||
Ruleset = { BindTarget = Ruleset },
|
||||
IsValidMod = _ => false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
|
||||
protected override ModSelectOverlay CreateModSelectOverlay() => modSelect = new UserModSelectOverlay(OverlayColourScheme.Plum)
|
||||
{
|
||||
IsValidMod = isValidRequiredMod
|
||||
IsValidMod = isValidRequiredMod,
|
||||
};
|
||||
|
||||
private PlaylistItem createItem() => new PlaylistItem(Beatmap.Value.BeatmapInfo)
|
||||
|
||||
@@ -28,6 +28,7 @@ using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Utils;
|
||||
@@ -120,10 +121,9 @@ namespace osu.Game.Screens.Select
|
||||
var judgementsStatistics = value.GetStatisticsForDisplay().Select(s =>
|
||||
new StatisticRow(s.DisplayName.ToUpper(), s.Count.ToLocalisableString("N0"), colours.ForHitResult(s.Result)));
|
||||
|
||||
double multiplier = 1.0;
|
||||
|
||||
foreach (var mod in value.Mods)
|
||||
multiplier *= mod.ScoreMultiplier;
|
||||
var ruleset = value.Ruleset.CreateInstance();
|
||||
var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
|
||||
double multiplier = scoreMultiplierCalculator.CalculateFor(value.Mods);
|
||||
|
||||
var generalStatistics = new[]
|
||||
{
|
||||
|
||||
@@ -13,7 +13,6 @@ using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
@@ -22,7 +21,9 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Footer;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Utils;
|
||||
@@ -32,7 +33,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public partial class FooterButtonMods : ScreenFooterButton, IHasCurrentValue<IReadOnlyList<Mod>>
|
||||
public partial class FooterButtonMods : ScreenFooterButton
|
||||
{
|
||||
public Action? RequestDeselectAllMods { get; init; }
|
||||
|
||||
@@ -40,12 +41,20 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private const float mod_display_portion = 0.65f;
|
||||
|
||||
private readonly BindableWithCurrent<IReadOnlyList<Mod>> current = new BindableWithCurrent<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||
private readonly BindableWithCurrent<IReadOnlyList<Mod>> mods = new BindableWithCurrent<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||
|
||||
public Bindable<IReadOnlyList<Mod>> Current
|
||||
public Bindable<IReadOnlyList<Mod>> Mods
|
||||
{
|
||||
get => current.Current;
|
||||
set => current.Current = value;
|
||||
get => mods.Current;
|
||||
set => mods.Current = value;
|
||||
}
|
||||
|
||||
private readonly BindableWithCurrent<RulesetInfo?> ruleset = new BindableWithCurrent<RulesetInfo?>();
|
||||
|
||||
public Bindable<RulesetInfo?> Ruleset
|
||||
{
|
||||
get => ruleset.Current;
|
||||
set => ruleset.Current = value;
|
||||
}
|
||||
|
||||
private Container modDisplayBar = null!;
|
||||
@@ -145,10 +154,10 @@ namespace osu.Game.Screens.Select
|
||||
Origin = Anchor.Centre,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Scale = new Vector2(0.5f),
|
||||
Current = { BindTarget = Current },
|
||||
Current = { BindTarget = Mods },
|
||||
ExpansionMode = ExpansionMode.AlwaysContracted,
|
||||
},
|
||||
overflowModCountDisplay = new ModCountText { Mods = { BindTarget = Current }, },
|
||||
overflowModCountDisplay = new ModCountText { Mods = { BindTarget = Mods }, },
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -165,7 +174,8 @@ namespace osu.Game.Screens.Select
|
||||
currentLanguage = game.CurrentLanguage.GetBoundCopy();
|
||||
currentLanguage.BindValueChanged(_ => ScheduleAfterChildren(updateDisplay));
|
||||
|
||||
Current.BindValueChanged(m =>
|
||||
Ruleset.BindValueChanged(_ => updateDisplay());
|
||||
Mods.BindValueChanged(m =>
|
||||
{
|
||||
modSettingChangeTracker?.Dispose();
|
||||
|
||||
@@ -198,7 +208,7 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
if (Current.Value.Count == 0)
|
||||
if (Mods.Value.Count == 0)
|
||||
{
|
||||
modDisplayBar.MoveToY(20, duration, easing);
|
||||
modDisplayBar.FadeOut(duration, easing);
|
||||
@@ -213,7 +223,7 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Current.Value.Any(m => !m.Ranked))
|
||||
if (Mods.Value.Any(m => !m.Ranked))
|
||||
{
|
||||
unrankedBadge.MoveToX(0, duration, easing);
|
||||
unrankedBadge.FadeIn(duration, easing);
|
||||
@@ -234,7 +244,8 @@ namespace osu.Game.Screens.Select
|
||||
modDisplay.FadeIn(duration, easing);
|
||||
}
|
||||
|
||||
double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1;
|
||||
var scoreMultiplierCalculator = Ruleset.Value?.CreateInstance().CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
|
||||
double multiplier = scoreMultiplierCalculator?.CalculateFor(Mods.Value) ?? 1;
|
||||
multiplierText.Text = ModUtils.FormatScoreMultiplier(multiplier);
|
||||
|
||||
if (multiplier > 1)
|
||||
@@ -249,7 +260,7 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (Current.Value.Count == 0)
|
||||
if (Mods.Value.Count == 0)
|
||||
return;
|
||||
|
||||
if (modDisplay.DrawWidth * modDisplay.Scale.X > modContainer.DrawWidth)
|
||||
|
||||
@@ -351,7 +351,8 @@ namespace osu.Game.Screens.Select
|
||||
new FooterButtonMods(modSelectOverlay)
|
||||
{
|
||||
Hotkey = GlobalAction.ToggleModSelection,
|
||||
Current = Mods,
|
||||
Mods = Mods,
|
||||
Ruleset = Ruleset,
|
||||
RequestDeselectAllMods = () =>
|
||||
{
|
||||
if (modSelectOverlay.State.Value == Visibility.Visible)
|
||||
@@ -708,6 +709,7 @@ namespace osu.Game.Screens.Select
|
||||
private void onArrivingAtScreen()
|
||||
{
|
||||
modSelectOverlay.Beatmap.BindTo(Beatmap);
|
||||
modSelectOverlay.Ruleset.BindTo(Ruleset);
|
||||
// required due to https://github.com/ppy/osu-framework/issues/3218
|
||||
modSelectOverlay.SelectedMods.Disabled = false;
|
||||
modSelectOverlay.SelectedMods.BindTo(Mods);
|
||||
@@ -755,6 +757,7 @@ namespace osu.Game.Screens.Select
|
||||
Beatmap.ValueChanged -= updateVariousState;
|
||||
|
||||
modSelectOverlay.SelectedMods.UnbindFrom(Mods);
|
||||
modSelectOverlay.Ruleset.UnbindFrom(Ruleset);
|
||||
modSelectOverlay.Beatmap.UnbindFrom(Beatmap);
|
||||
|
||||
updateWedgeVisibility();
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace osu.Game.Storyboards
|
||||
private readonly Dictionary<string, StoryboardLayer> layers = new Dictionary<string, StoryboardLayer>();
|
||||
public IEnumerable<StoryboardLayer> Layers => layers.Values;
|
||||
|
||||
public BeatmapInfo BeatmapInfo = new BeatmapInfo();
|
||||
public BeatmapInfo BeatmapInfo { get; set; } = new BeatmapInfo();
|
||||
public IBeatmap Beatmap { get; set; } = new Beatmap();
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Tests.Rulesets
|
||||
{
|
||||
@@ -21,33 +22,20 @@ namespace osu.Game.Tests.Rulesets
|
||||
|
||||
[Test]
|
||||
public void TestDefaultMultiplierIsOne()
|
||||
{
|
||||
var calculator = Ruleset.CreateScoreMultiplierCalculator();
|
||||
Assert.That(calculator.CalculateFor([]), Is.EqualTo(1));
|
||||
}
|
||||
=> TestModCombination([], 1);
|
||||
|
||||
[Test]
|
||||
public void TestMultipliersMatchForIndividualMods()
|
||||
protected void TestModCombination(IEnumerable<Mod> mods, double expectedMultiplier)
|
||||
{
|
||||
var mods = Ruleset.CreateAllMods();
|
||||
var calculator = Ruleset.CreateScoreMultiplierCalculator();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
double multiplierViaOldAPI = 1;
|
||||
foreach (var mod in mods)
|
||||
Assert.That(calculator.CalculateFor(mod.Yield()), Is.EqualTo(mod.ScoreMultiplier), message: $"Score multiplier not matching for mod {mod.Name}");
|
||||
multiplierViaOldAPI *= mod.ScoreMultiplier;
|
||||
Assert.That(multiplierViaOldAPI, Is.EqualTo(expectedMultiplier).Within(Precision.DOUBLE_EPSILON));
|
||||
|
||||
var calculator = Ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
|
||||
Assert.That(calculator.CalculateFor(mods), Is.EqualTo(expectedMultiplier).Within(Precision.DOUBLE_EPSILON));
|
||||
});
|
||||
}
|
||||
|
||||
protected void TestModCombination(IEnumerable<Mod> mods)
|
||||
{
|
||||
var calculator = Ruleset.CreateScoreMultiplierCalculator();
|
||||
|
||||
double expected = 1;
|
||||
foreach (var mod in mods)
|
||||
expected *= mod.ScoreMultiplier;
|
||||
|
||||
Assert.That(calculator.CalculateFor(mods), Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,15 +56,8 @@ namespace osu.Game.Updater
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
string version = game.Version;
|
||||
string lastVersion = config.Get<string>(OsuSetting.Version);
|
||||
|
||||
if (game.IsDeployedBuild)
|
||||
{
|
||||
// only show a notification if we've previously saved a version to the config file (ie. not the first run).
|
||||
if (!string.IsNullOrEmpty(lastVersion) && version != lastVersion)
|
||||
Notifications.Post(new UpdateCompleteNotification(version));
|
||||
|
||||
// make sure the release stream setting matches the build which was just run.
|
||||
if (FixedReleaseStream != null)
|
||||
config.SetValue(OsuSetting.ReleaseStream, FixedReleaseStream.Value);
|
||||
@@ -137,31 +130,6 @@ namespace osu.Game.Updater
|
||||
updateCancellationSource.Dispose();
|
||||
}
|
||||
|
||||
private partial class UpdateCompleteNotification : SimpleNotification
|
||||
{
|
||||
private readonly string version;
|
||||
|
||||
public UpdateCompleteNotification(string version)
|
||||
{
|
||||
this.version = version;
|
||||
Text = NotificationsStrings.GameVersionAfterUpdate(version);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, ChangelogOverlay changelog, INotificationOverlay notificationOverlay)
|
||||
{
|
||||
Icon = FontAwesome.Solid.CheckSquare;
|
||||
IconContent.Colour = colours.BlueDark;
|
||||
|
||||
Activated = delegate
|
||||
{
|
||||
notificationOverlay.Hide();
|
||||
changelog.ShowBuild(version);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public partial class UpdateDownloadProgressNotification : ProgressNotification
|
||||
{
|
||||
private readonly CancellationToken cancellationToken;
|
||||
@@ -259,4 +227,29 @@ namespace osu.Game.Updater
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class UpdateCompleteNotification : SimpleNotification
|
||||
{
|
||||
private readonly string version;
|
||||
|
||||
public UpdateCompleteNotification(string version)
|
||||
{
|
||||
this.version = version;
|
||||
Text = NotificationsStrings.GameVersionAfterUpdate(version);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, ChangelogOverlay changelog, INotificationOverlay notificationOverlay)
|
||||
{
|
||||
Icon = FontAwesome.Solid.CheckSquare;
|
||||
IconContent.Colour = colours.BlueDark;
|
||||
|
||||
Activated = delegate
|
||||
{
|
||||
notificationOverlay.Hide();
|
||||
changelog.ShowBuild(version);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="20.1.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2026.521.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2026.521.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2026.523.0" />
|
||||
<PackageReference Include="Sentry" Version="6.2.0" />
|
||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||
<PackageReference Include="SharpCompress" Version="0.48.0" />
|
||||
|
||||
Reference in New Issue
Block a user