mirror of
https://github.com/ppy/osu.git
synced 2026-05-20 09:20:15 +08:00
Compare commits
438 Commits
@@ -18,3 +18,6 @@ M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize(
|
||||
M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead.
|
||||
M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead.
|
||||
M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToKebabCase() instead.
|
||||
M:osuTK.MathHelper.Clamp(System.Int32,System.Int32,System.Int32)~System.Int32;Use Math.Clamp() instead.
|
||||
M:osuTK.MathHelper.Clamp(System.Single,System.Single,System.Single)~System.Single;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead.
|
||||
M:osuTK.MathHelper.Clamp(System.Double,System.Double,System.Double)~System.Double;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead.
|
||||
|
||||
+2
-2
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -9,7 +10,6 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Pippidon.Objects;
|
||||
using osu.Game.Rulesets.Pippidon.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Pippidon.Beatmaps
|
||||
{
|
||||
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Pippidon.Beatmaps
|
||||
};
|
||||
}
|
||||
|
||||
private int getLane(HitObject hitObject) => (int)MathHelper.Clamp(
|
||||
private int getLane(HitObject hitObject) => (int)Math.Clamp(
|
||||
(getUsablePosition(hitObject) - minPosition) / (maxPosition - minPosition) * PippidonPlayfield.LANE_COUNT, 0, PippidonPlayfield.LANE_COUNT - 1);
|
||||
|
||||
private float getUsablePosition(HitObject h) => (h as IHasYPosition)?.Y ?? ((IHasXPosition)h).X;
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.225.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.313.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
public abstract partial class CatchPlacementBlueprintTestScene : PlacementBlueprintTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreateRuleset() => new CatchRuleset();
|
||||
|
||||
protected const double TIME_SNAP = 100;
|
||||
|
||||
protected DrawableCatchHitObject LastObject;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
|
||||
@@ -10,15 +9,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
public class CatchDifficultyAttributes : DifficultyAttributes
|
||||
{
|
||||
/// <summary>
|
||||
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("approach_rate")]
|
||||
public double ApproachRate { get; set; }
|
||||
|
||||
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
||||
{
|
||||
foreach (var v in base.ToDatabaseAttributes())
|
||||
@@ -26,7 +16,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
// Todo: osu!catch should not output star rating in the 'aim' attribute.
|
||||
yield return (ATTRIB_ID_AIM, StarRating);
|
||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
@@ -34,7 +23,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
StarRating = values[ATTRIB_ID_AIM];
|
||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
||||
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
private float halfCatcherWidth;
|
||||
|
||||
public override int Version => 20220701;
|
||||
public override int Version => 20250306;
|
||||
|
||||
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
@@ -35,14 +36,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
return new CatchDifficultyAttributes { Mods = mods };
|
||||
|
||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
CatchDifficultyAttributes attributes = new CatchDifficultyAttributes
|
||||
{
|
||||
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier,
|
||||
StarRating = Math.Sqrt(skills.OfType<Movement>().Single().DifficultyValue()) * difficulty_multiplier,
|
||||
Mods = mods,
|
||||
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
@@ -50,7 +53,19 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
if (catchAttributes.MaxCombo > 0)
|
||||
value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0);
|
||||
|
||||
double approachRate = catchAttributes.ApproachRate;
|
||||
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
|
||||
|
||||
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
||||
|
||||
var track = new TrackVirtual(10000);
|
||||
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
double clockRate = track.Rate;
|
||||
|
||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
double approachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0;
|
||||
|
||||
double approachRateFactor = 1.0;
|
||||
if (approachRate > 9.0)
|
||||
approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9
|
||||
|
||||
@@ -26,7 +26,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
|
||||
private float? lastPlayerPosition;
|
||||
private float lastDistanceMoved;
|
||||
private float lastExactDistanceMoved;
|
||||
private double lastStrainTime;
|
||||
private bool isInBuzzSection;
|
||||
|
||||
/// <summary>
|
||||
/// The speed multiplier applied to the player's catcher.
|
||||
@@ -59,6 +61,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
|
||||
float distanceMoved = playerPosition - lastPlayerPosition.Value;
|
||||
|
||||
// For the exact position we consider that the catcher is in the correct position for both objects
|
||||
float exactDistanceMoved = catchCurrent.NormalizedPosition - lastPlayerPosition.Value;
|
||||
|
||||
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
|
||||
|
||||
double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
|
||||
@@ -92,12 +97,30 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
playerPosition = catchCurrent.NormalizedPosition;
|
||||
}
|
||||
|
||||
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
||||
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20)
|
||||
* Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
||||
}
|
||||
|
||||
// There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than
|
||||
// the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and normalized_hitobject_radius offsets
|
||||
// We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified.
|
||||
// To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius)
|
||||
if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime)
|
||||
{
|
||||
if (isInBuzzSection)
|
||||
distanceAddition = 0;
|
||||
else
|
||||
isInBuzzSection = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isInBuzzSection = false;
|
||||
}
|
||||
|
||||
lastPlayerPosition = playerPosition;
|
||||
lastDistanceMoved = distanceMoved;
|
||||
lastStrainTime = catchCurrent.StrainTime;
|
||||
lastExactDistanceMoved = exactDistanceMoved;
|
||||
|
||||
return distanceAddition / weightedStrainTime;
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
{
|
||||
base.UpdateHitObjectFromPath(hitObject);
|
||||
|
||||
if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLength)
|
||||
if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLengthForPlacement)
|
||||
EditorBeatmap?.Remove(hitObject);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -110,7 +111,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Argon
|
||||
|
||||
double duration = ObjectState.HitObject.StartTime - ObjectState.DisplayStartTime;
|
||||
|
||||
fadeContent.Alpha = MathHelper.Clamp(
|
||||
fadeContent.Alpha = Math.Clamp(
|
||||
Interpolation.ValueAt(
|
||||
Time.Current, 1f, 0f,
|
||||
ObjectState.DisplayStartTime + duration * lens_flare_start,
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public abstract partial class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreateRuleset() => new ManiaRuleset();
|
||||
|
||||
private readonly Column column;
|
||||
|
||||
[Cached(typeof(IReadOnlyList<Mod>))]
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
keyCount.Current.Value = 8;
|
||||
});
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().SingleOrDefault()?.CurrentDialog, Is.InstanceOf<ReloadEditorDialog>);
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().SingleOrDefault()?.CurrentDialog, Is.InstanceOf<SaveAndReloadEditorDialog>);
|
||||
AddStep("refuse", () => InputManager.Key(Key.Number2));
|
||||
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
keyCount.Current.Value = 8;
|
||||
});
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().Single().CurrentDialog, Is.InstanceOf<ReloadEditorDialog>);
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().Single().CurrentDialog, Is.InstanceOf<SaveAndReloadEditorDialog>);
|
||||
AddStep("acquiesce", () => InputManager.Key(Key.Number1));
|
||||
AddUntilStep("beatmap became 8K", () => Game.Beatmap.Value.BeatmapInfo.Difficulty.CircleSize, () => Is.EqualTo(8));
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
@@ -14,6 +17,11 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() => toggleTouchControls(false));
|
||||
|
||||
#region Without touch controls
|
||||
|
||||
[Test]
|
||||
public void TestTouchInput()
|
||||
{
|
||||
@@ -63,6 +71,79 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region With touch controls
|
||||
|
||||
[Test]
|
||||
public void TestTouchAreaNotInitiallyVisible()
|
||||
{
|
||||
AddStep("enable touch controls", () => toggleTouchControls(true));
|
||||
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPressReceptors()
|
||||
{
|
||||
AddStep("enable touch controls", () => toggleTouchControls(true));
|
||||
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
int index = i;
|
||||
|
||||
AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getReceptor(index).Action.Value));
|
||||
|
||||
AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestColumnsNotTouchableWithTouchControls()
|
||||
{
|
||||
AddStep("enable touch controls", () => toggleTouchControls(true));
|
||||
|
||||
AddStep("touch receptor 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getReceptor(0).Action.Value));
|
||||
|
||||
AddStep("release receptor 0", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible);
|
||||
|
||||
AddStep("touch column 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre + new Vector2(0f, -50f))));
|
||||
|
||||
AddAssert("action not sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||
|
||||
AddStep("release column 0", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre + new Vector2(0f, -50f))));
|
||||
|
||||
AddAssert("action not sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void toggleTouchControls(bool enabled)
|
||||
{
|
||||
var maniaConfig = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(CreatePlayerRuleset())!;
|
||||
maniaConfig.SetValue(ManiaRulesetSetting.MobileLayout, enabled ? ManiaMobileLayout.LandscapeWithOverlay : ManiaMobileLayout.Portrait);
|
||||
}
|
||||
|
||||
private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType<ManiaTouchInputArea>().SingleOrDefault();
|
||||
|
||||
private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType<ManiaTouchInputArea.ColumnInputReceptor>().ElementAt(index);
|
||||
|
||||
private Column getColumn(int index) => this.ChildrenOfType<Column>().ElementAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
SetDefault(ManiaRulesetSetting.ScrollSpeed, 8.0, 1.0, 40.0, 0.1);
|
||||
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
|
||||
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
|
||||
SetDefault(ManiaRulesetSetting.MobileLayout, ManiaMobileLayout.Portrait);
|
||||
|
||||
#pragma warning disable CS0618
|
||||
// Although obsolete, this is still required to populate the bindable from the database in case migration is required.
|
||||
@@ -55,6 +56,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
ScrollTime,
|
||||
ScrollSpeed,
|
||||
ScrollDirection,
|
||||
TimingBasedNoteColouring
|
||||
TimingBasedNoteColouring,
|
||||
MobileLayout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
|
||||
@@ -10,22 +9,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
public class ManiaDifficultyAttributes : DifficultyAttributes
|
||||
{
|
||||
/// <summary>
|
||||
/// The hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods do not affect the hit window at all in osu-stable.
|
||||
/// </remarks>
|
||||
[JsonProperty("great_hit_window")]
|
||||
public double GreatHitWindow { get; set; }
|
||||
|
||||
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
||||
{
|
||||
foreach (var v in base.ToDatabaseAttributes())
|
||||
yield return v;
|
||||
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
@@ -33,7 +22,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
private const double difficulty_multiplier = 0.018;
|
||||
|
||||
private readonly bool isForCurrentRuleset;
|
||||
private readonly double originalOverallDifficulty;
|
||||
|
||||
public override int Version => 20241007;
|
||||
|
||||
@@ -35,7 +34,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
: base(ruleset, beatmap)
|
||||
{
|
||||
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
|
||||
originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty;
|
||||
}
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
@@ -48,11 +46,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
|
||||
ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes
|
||||
{
|
||||
StarRating = skills[0].DifficultyValue() * difficulty_multiplier,
|
||||
StarRating = skills.OfType<Strain>().Single().DifficultyValue() * difficulty_multiplier,
|
||||
Mods = mods,
|
||||
// In osu-stable mania, rate-adjustment mods don't affect the hit window.
|
||||
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
|
||||
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
|
||||
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject),
|
||||
};
|
||||
|
||||
@@ -124,29 +119,5 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private double getHitWindow300(Mod[] mods)
|
||||
{
|
||||
if (isForCurrentRuleset)
|
||||
{
|
||||
double od = Math.Min(10.0, Math.Max(0, 10.0 - originalOverallDifficulty));
|
||||
return applyModAdjustments(34 + 3 * od, mods);
|
||||
}
|
||||
|
||||
if (Math.Round(originalOverallDifficulty) > 4)
|
||||
return applyModAdjustments(34, mods);
|
||||
|
||||
return applyModAdjustments(47, mods);
|
||||
|
||||
static double applyModAdjustments(double value, Mod[] mods)
|
||||
{
|
||||
if (mods.Any(m => m is ManiaModHardRock))
|
||||
value /= 1.4;
|
||||
else if (mods.Any(m => m is ManiaModEasy))
|
||||
value *= 1.4;
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
|
||||
updatingKeyCount = true;
|
||||
|
||||
editor.Reload().ContinueWith(t =>
|
||||
editor.SaveAndReload().ContinueWith(t =>
|
||||
{
|
||||
if (!t.GetResultSafely())
|
||||
{
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
public enum ManiaMobileLayout
|
||||
{
|
||||
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.PortraitExpandedColumns))]
|
||||
Portrait,
|
||||
|
||||
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.LandscapeExpandedColumns))]
|
||||
Landscape,
|
||||
|
||||
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.LandscapeTouchOverlay))]
|
||||
LandscapeWithOverlay,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
@@ -44,8 +45,17 @@ namespace osu.Game.Rulesets.Mania
|
||||
Keywords = new[] { "color" },
|
||||
LabelText = RulesetSettingsStrings.TimingBasedColouring,
|
||||
Current = config.GetBindable<bool>(ManiaRulesetSetting.TimingBasedNoteColouring),
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (RuntimeInfo.IsMobile)
|
||||
{
|
||||
Add(new SettingsEnumDropdown<ManiaMobileLayout>
|
||||
{
|
||||
LabelText = RulesetSettingsStrings.MobileLayout,
|
||||
Current = config.GetBindable<ManiaMobileLayout>(ManiaRulesetSetting.MobileLayout),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private partial class ManiaScrollSlider : RoundedSliderBar<double>
|
||||
|
||||
@@ -42,8 +42,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
var locations = column.OfType<Note>().Select(n => (startTime: n.StartTime, samples: n.Samples))
|
||||
.Concat(column.OfType<HoldNote>().SelectMany(h => new[]
|
||||
{
|
||||
(startTime: h.StartTime, samples: h.GetNodeSamples(0)),
|
||||
(startTime: h.EndTime, samples: h.GetNodeSamples(1))
|
||||
(startTime: h.StartTime, samples: h.GetNodeSamples(0))
|
||||
}))
|
||||
.OrderBy(h => h.startTime).ToList();
|
||||
|
||||
|
||||
@@ -26,9 +26,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
: base(barLine)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 1;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.BarLine), _ => new DefaultBarLine())
|
||||
@@ -36,8 +37,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
|
||||
Major.BindValueChanged(major => Height = major.NewValue ? 1.7f : 1.2f, true);
|
||||
}
|
||||
|
||||
protected override void OnApply()
|
||||
|
||||
@@ -53,7 +53,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
}
|
||||
}
|
||||
|
||||
private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
|
||||
private void onDirectionChanged()
|
||||
{
|
||||
Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
|
||||
}
|
||||
|
||||
protected override SpriteText CreateJudgementText() =>
|
||||
new OsuSpriteText
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject drawableHitObject)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
|
||||
// Avoid flickering due to no anti-aliasing of boxes by default.
|
||||
var edgeSmoothness = new Vector2(0.3f);
|
||||
@@ -75,6 +75,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default
|
||||
|
||||
private void updateMajor(ValueChangedEvent<bool> major)
|
||||
{
|
||||
Height = major.NewValue ? 1.7f : 1.2f;
|
||||
|
||||
mainLine.Alpha = major.NewValue ? 0.5f : 0.2f;
|
||||
leftAnchor.Alpha = rightAnchor.Alpha = major.NewValue ? mainLine.Alpha * 0.3f : 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
{
|
||||
public partial class LegacyBarLine : CompositeDrawable
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
float skinHeight = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.BarLineHeight)?.Value ?? 1;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 1.2f * skinHeight;
|
||||
Colour = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.BarLineColour)?.Value ?? Color4.White;
|
||||
|
||||
// Avoid flickering due to no anti-aliasing of boxes by default.
|
||||
var edgeSmoothness = new Vector2(0.3f);
|
||||
|
||||
AddInternal(new Box
|
||||
{
|
||||
Name = "Bar line",
|
||||
EdgeSmoothness = edgeSmoothness,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
this.result = result;
|
||||
this.animation = animation;
|
||||
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
@@ -53,10 +52,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
float hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0;
|
||||
float scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0;
|
||||
|
||||
float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition;
|
||||
float finalPosition = scorePosition - absoluteHitPosition;
|
||||
float hitPositionFromTop = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition;
|
||||
|
||||
Y = direction.Value == ScrollingDirection.Up ? -finalPosition : finalPosition;
|
||||
if (scorePosition > hitPositionFromTop / 2f)
|
||||
{
|
||||
Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
Y = direction.Value == ScrollingDirection.Up ? hitPositionFromTop - scorePosition : scorePosition - hitPositionFromTop;
|
||||
}
|
||||
else
|
||||
{
|
||||
Anchor = direction.Value == ScrollingDirection.Up ? Anchor.BottomCentre : Anchor.TopCentre;
|
||||
Y = direction.Value == ScrollingDirection.Up ? -scorePosition : scorePosition;
|
||||
}
|
||||
}
|
||||
|
||||
public void PlayAnimation()
|
||||
|
||||
@@ -163,7 +163,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
return new LegacyStageForeground();
|
||||
|
||||
case ManiaSkinComponents.BarLine:
|
||||
return null; // Not yet implemented.
|
||||
return new LegacyBarLine();
|
||||
|
||||
default:
|
||||
throw new UnsupportedSkinComponentException(lookup);
|
||||
|
||||
@@ -12,6 +12,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
@@ -57,6 +58,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>(Color4.Black);
|
||||
|
||||
private IBindable<ManiaMobileLayout> mobilePlayStyle = null!;
|
||||
|
||||
public Column(int index, bool isSpecial)
|
||||
{
|
||||
Index = index;
|
||||
@@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
private ISkinSource skin { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host)
|
||||
private void load(GameHost host, ManiaRulesetConfigManager? rulesetConfig)
|
||||
{
|
||||
SkinnableDrawable keyArea;
|
||||
|
||||
@@ -115,6 +118,9 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50);
|
||||
RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50);
|
||||
RegisterPool<HoldNoteBody, DrawableHoldNoteBody>(10, 50);
|
||||
|
||||
if (rulesetConfig != null)
|
||||
mobilePlayStyle = rulesetConfig.GetBindable<ManiaMobileLayout>(ManiaRulesetSetting.MobileLayout);
|
||||
}
|
||||
|
||||
private void onSourceChanged()
|
||||
@@ -193,6 +199,10 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
protected override bool OnTouchDown(TouchDownEvent e)
|
||||
{
|
||||
// if touch overlay is visible, disallow columns from handling touch directly.
|
||||
if (mobilePlayStyle.Value == ManiaMobileLayout.LandscapeWithOverlay)
|
||||
return false;
|
||||
|
||||
maniaInputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
|
||||
touchActivationCount++;
|
||||
return true;
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
@@ -34,6 +39,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
set => base.Masking = value;
|
||||
}
|
||||
|
||||
private readonly LayoutValue layout = new LayoutValue(Invalidation.DrawSize);
|
||||
|
||||
public ColumnFlow(StageDefinition stageDefinition)
|
||||
{
|
||||
this.stageDefinition = stageDefinition;
|
||||
@@ -52,42 +59,32 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
for (int i = 0; i < stageDefinition.Columns; i++)
|
||||
columns.Add(new Container<TContent> { RelativeSizeAxes = Axes.Y });
|
||||
|
||||
AddLayout(layout);
|
||||
}
|
||||
|
||||
private ISkinSource currentSkin;
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; } = null!;
|
||||
|
||||
private readonly Bindable<ManiaMobileLayout> mobileLayout = new Bindable<ManiaMobileLayout>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
private void load(ManiaRulesetConfigManager? rulesetConfig)
|
||||
{
|
||||
currentSkin = skin;
|
||||
rulesetConfig?.BindWith(ManiaRulesetSetting.MobileLayout, mobileLayout);
|
||||
|
||||
skin.SourceChanged += onSkinChanged;
|
||||
onSkinChanged();
|
||||
mobileLayout.BindValueChanged(_ => invalidateLayout());
|
||||
skin.SourceChanged += invalidateLayout;
|
||||
}
|
||||
|
||||
private void onSkinChanged()
|
||||
protected override void Update()
|
||||
{
|
||||
for (int i = 0; i < stageDefinition.Columns; i++)
|
||||
base.Update();
|
||||
|
||||
if (!layout.IsValid)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
float spacing = currentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1))
|
||||
?.Value ?? Stage.COLUMN_SPACING;
|
||||
|
||||
columns[i].Margin = new MarginPadding { Left = spacing };
|
||||
}
|
||||
|
||||
float? width = currentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i))
|
||||
?.Value;
|
||||
|
||||
bool isSpecialColumn = stageDefinition.IsSpecialColumn(i);
|
||||
|
||||
// only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration)
|
||||
width ??= isSpecialColumn ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH;
|
||||
|
||||
columns[i].Width = width.Value;
|
||||
updateColumnSize();
|
||||
layout.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,12 +98,60 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
Content[column] = columns[column].Child = content;
|
||||
}
|
||||
|
||||
private void invalidateLayout() => layout.Invalidate();
|
||||
|
||||
private void updateColumnSize()
|
||||
{
|
||||
float mobileAdjust = 1f;
|
||||
|
||||
if (RuntimeInfo.IsMobile && mobileLayout.Value == ManiaMobileLayout.Landscape)
|
||||
{
|
||||
// GridContainer+CellContainer containing this stage (gets split up for dual stages).
|
||||
Vector2? containingCell = this.FindClosestParent<Stage>()?.Parent?.DrawSize;
|
||||
|
||||
// Will be null in tests.
|
||||
if (containingCell != null && containingCell.Value.X >= containingCell.Value.Y)
|
||||
{
|
||||
float aspectRatio = containingCell.Value.X / containingCell.Value.Y;
|
||||
|
||||
// 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon)
|
||||
mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns);
|
||||
// 1.92 is a "reference" mobile screen aspect ratio for phones.
|
||||
// We should scale it back for cases like tablets which aren't so extreme.
|
||||
mobileAdjust *= aspectRatio / 1.92f;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < stageDefinition.Columns; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
float spacing = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1))
|
||||
?.Value ?? Stage.COLUMN_SPACING;
|
||||
|
||||
columns[i].Margin = new MarginPadding { Left = spacing };
|
||||
}
|
||||
|
||||
float? width = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i))
|
||||
?.Value;
|
||||
|
||||
bool isSpecialColumn = stageDefinition.IsSpecialColumn(i);
|
||||
|
||||
// only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration)
|
||||
width ??= isSpecialColumn ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH;
|
||||
|
||||
columns[i].Width = width.Value * mobileAdjust;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (currentSkin != null)
|
||||
currentSkin.SourceChanged -= onSkinChanged;
|
||||
if (skin.IsNotNull())
|
||||
skin.SourceChanged -= invalidateLayout;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
direction.BindValueChanged(_ => onDirectionChanged(), true);
|
||||
}
|
||||
|
||||
private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
|
||||
private void onDirectionChanged()
|
||||
{
|
||||
Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
|
||||
@@ -3,30 +3,23 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
public partial class DrawableManiaJudgement : DrawableJudgement
|
||||
{
|
||||
private IBindable<ScrollingDirection> direction;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
public DrawableManiaJudgement()
|
||||
{
|
||||
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||
direction.BindValueChanged(_ => onDirectionChanged(), true);
|
||||
}
|
||||
|
||||
private void onDirectionChanged()
|
||||
{
|
||||
Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
Origin = Anchor.Centre;
|
||||
// Extend the dimensions of this drawable to the entire parenting container.
|
||||
// This allows skin implementations (i.e. LegacyManiaJudgementPiece) to freely choose the anchor based on skin settings.
|
||||
Anchor = Anchor.TopLeft;
|
||||
Origin = Anchor.TopLeft;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Size = new Vector2(1f);
|
||||
}
|
||||
|
||||
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
public IEnumerable<BarLine> BarLines;
|
||||
|
||||
public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1;
|
||||
public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1 && mobileLayout.Value == ManiaMobileLayout.Portrait;
|
||||
|
||||
protected override bool RelativeScaleBeatLengths => true;
|
||||
|
||||
@@ -58,6 +58,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
|
||||
private readonly BindableDouble configScrollSpeed = new BindableDouble();
|
||||
private readonly Bindable<ManiaMobileLayout> mobileLayout = new Bindable<ManiaMobileLayout>();
|
||||
|
||||
private double currentTimeRange;
|
||||
protected double TargetTimeRange;
|
||||
@@ -111,6 +112,28 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue));
|
||||
|
||||
TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value);
|
||||
|
||||
Config.BindWith(ManiaRulesetSetting.MobileLayout, mobileLayout);
|
||||
mobileLayout.BindValueChanged(_ => updateMobileLayout(), true);
|
||||
}
|
||||
|
||||
private ManiaTouchInputArea? touchInputArea;
|
||||
|
||||
private void updateMobileLayout()
|
||||
{
|
||||
switch (mobileLayout.Value)
|
||||
{
|
||||
case ManiaMobileLayout.LandscapeWithOverlay:
|
||||
KeyBindingInputManager.Add(touchInputArea = new ManiaTouchInputArea(this));
|
||||
break;
|
||||
|
||||
default:
|
||||
if (touchInputArea != null)
|
||||
KeyBindingInputManager.Remove(touchInputArea, true);
|
||||
|
||||
touchInputArea = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// An overlay that captures and displays osu!mania mouse and touch input.
|
||||
/// </summary>
|
||||
public partial class ManiaTouchInputArea : VisibilityContainer
|
||||
{
|
||||
private readonly DrawableManiaRuleset drawableRuleset;
|
||||
|
||||
// visibility state affects our child. we always want to handle input.
|
||||
public override bool PropagatePositionalInputSubTree => true;
|
||||
public override bool PropagateNonPositionalInputSubTree => true;
|
||||
|
||||
[SettingSource("Spacing", "The spacing between receptors.")]
|
||||
public BindableFloat Spacing { get; } = new BindableFloat(10)
|
||||
{
|
||||
Precision = 1,
|
||||
MinValue = 0,
|
||||
MaxValue = 100,
|
||||
};
|
||||
|
||||
[SettingSource("Opacity", "The receptor opacity.")]
|
||||
public BindableFloat Opacity { get; } = new BindableFloat(1)
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 0,
|
||||
MaxValue = 1
|
||||
};
|
||||
|
||||
private GridContainer gridContainer = null!;
|
||||
|
||||
public ManiaTouchInputArea(DrawableManiaRuleset drawableRuleset)
|
||||
{
|
||||
this.drawableRuleset = drawableRuleset;
|
||||
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Origin = Anchor.BottomCentre;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Height = 0.5f;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
List<Drawable> receptorGridContent = new List<Drawable>();
|
||||
List<Dimension> receptorGridDimensions = new List<Dimension>();
|
||||
|
||||
bool first = true;
|
||||
|
||||
foreach (var stage in drawableRuleset.Playfield.Stages)
|
||||
{
|
||||
foreach (var column in stage.Columns)
|
||||
{
|
||||
if (!first)
|
||||
{
|
||||
receptorGridContent.Add(new Gutter { Spacing = { BindTarget = Spacing } });
|
||||
receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize));
|
||||
}
|
||||
|
||||
receptorGridContent.Add(new ColumnInputReceptor
|
||||
{
|
||||
Action = { BindTarget = column.Action },
|
||||
});
|
||||
receptorGridDimensions.Add(new Dimension());
|
||||
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
InternalChild = gridContainer = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
AlwaysPresent = true,
|
||||
Content = new[] { receptorGridContent.ToArray() },
|
||||
ColumnDimensions = receptorGridDimensions.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
Opacity.BindValueChanged(o => Alpha = o.NewValue, true);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
// Hide whenever the keyboard is used.
|
||||
Hide();
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override bool OnTouchDown(TouchDownEvent e)
|
||||
{
|
||||
Show();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
gridContainer.FadeIn(500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
gridContainer.FadeOut(300);
|
||||
}
|
||||
|
||||
public partial class ColumnInputReceptor : CompositeDrawable
|
||||
{
|
||||
public readonly IBindable<ManiaAction> Action = new Bindable<ManiaAction>();
|
||||
|
||||
private readonly Box highlightOverlay;
|
||||
|
||||
[Resolved]
|
||||
private ManiaInputManager? inputManager { get; set; }
|
||||
|
||||
private bool isPressed;
|
||||
|
||||
public ColumnInputReceptor()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 10,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.15f,
|
||||
},
|
||||
highlightOverlay = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
Blending = BlendingParameters.Additive,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool OnTouchDown(TouchDownEvent e)
|
||||
{
|
||||
updateButton(true);
|
||||
return false; // handled by parent container to show overlay.
|
||||
}
|
||||
|
||||
protected override void OnTouchUp(TouchUpEvent e)
|
||||
{
|
||||
updateButton(false);
|
||||
}
|
||||
|
||||
private void updateButton(bool press)
|
||||
{
|
||||
if (press == isPressed)
|
||||
return;
|
||||
|
||||
isPressed = press;
|
||||
|
||||
if (press)
|
||||
{
|
||||
inputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
|
||||
highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
inputManager?.KeyBindingContainer.TriggerReleased(Action.Value);
|
||||
highlightOverlay.FadeTo(0, 400, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private partial class Gutter : Drawable
|
||||
{
|
||||
public readonly IBindable<float> Spacing = new Bindable<float>();
|
||||
|
||||
public Gutter()
|
||||
{
|
||||
Spacing.BindValueChanged(s => Size = new Vector2(s.NewValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreateRuleset() => new OsuRuleset();
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject);
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreateRuleset() => new OsuRuleset();
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneSpinnerPlacementBlueprint : PlacementBlueprintTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreateRuleset() => new OsuRuleset();
|
||||
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSpinner((Spinner)hitObject);
|
||||
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint();
|
||||
|
||||
@@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
|
||||
|
||||
[TestCase(6.7171144000821119d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
|
||||
[TestCase(6.7331304290522747d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4602604078137214d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
|
||||
[TestCase(0.14143808967817237d, 2, "nan-slider")]
|
||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||
|
||||
[TestCase(8.9825709931204205d, 239, "diffcalc-test")]
|
||||
[TestCase(1.7550169162648608d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.55231632896800109d, 4, "very-fast-slider")]
|
||||
[TestCase(9.6779746353001634d, 239, "diffcalc-test")]
|
||||
[TestCase(1.7691451263718989d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.55785578988249407d, 4, "very-fast-slider")]
|
||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
||||
|
||||
[TestCase(6.7171144000821119d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
|
||||
[TestCase(6.7331304290522747d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4602604078137214d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
|
||||
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
||||
|
||||
|
||||
@@ -12,9 +12,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
public static class AimEvaluator
|
||||
{
|
||||
private const double wide_angle_multiplier = 1.5;
|
||||
private const double acute_angle_multiplier = 1.95;
|
||||
private const double acute_angle_multiplier = 2.6;
|
||||
private const double slider_multiplier = 1.35;
|
||||
private const double velocity_change_multiplier = 0.75;
|
||||
private const double wiggle_multiplier = 1.02;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the difficulty of aiming the current object, based on:
|
||||
@@ -64,16 +65,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
double acuteAngleBonus = 0;
|
||||
double sliderBonus = 0;
|
||||
double velocityChangeBonus = 0;
|
||||
double wiggleBonus = 0;
|
||||
|
||||
double aimStrain = currVelocity; // Start strain with regular velocity.
|
||||
|
||||
if (Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime) < 1.25 * Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime)) // If rhythms are the same.
|
||||
{
|
||||
if (osuCurrObj.Angle != null && osuLastObj.Angle != null && osuLastLastObj.Angle != null)
|
||||
if (osuCurrObj.Angle != null && osuLastObj.Angle != null)
|
||||
{
|
||||
double currAngle = osuCurrObj.Angle.Value;
|
||||
double lastAngle = osuLastObj.Angle.Value;
|
||||
double lastLastAngle = osuLastLastObj.Angle.Value;
|
||||
|
||||
// Rewarding angles, take the smaller velocity as base.
|
||||
double angleBonus = Math.Min(currVelocity, prevVelocity);
|
||||
@@ -81,20 +82,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
wideAngleBonus = calcWideAngleBonus(currAngle);
|
||||
acuteAngleBonus = calcAcuteAngleBonus(currAngle);
|
||||
|
||||
if (DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2) < 300) // Only buff deltaTime exceeding 300 bpm 1/2.
|
||||
acuteAngleBonus = 0;
|
||||
else
|
||||
{
|
||||
acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
|
||||
* Math.Min(angleBonus, diameter * 1.25 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, radius, diameter) - radius) / radius), 2); // Buff distance exceeding radius up to diameter.
|
||||
}
|
||||
// Penalize angle repetition.
|
||||
wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3));
|
||||
acuteAngleBonus *= 0.08 + 0.92 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
|
||||
|
||||
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
|
||||
wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)));
|
||||
// Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse.
|
||||
acuteAngleBonus *= 0.5 + 0.5 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3)));
|
||||
// Apply full wide angle bonus for distance more than one diameter
|
||||
wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter);
|
||||
|
||||
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
||||
acuteAngleBonus *= angleBonus *
|
||||
DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) *
|
||||
DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2);
|
||||
|
||||
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
||||
// https://www.desmos.com/calculator/dp0v0nvowc
|
||||
wiggleBonus = angleBonus
|
||||
* DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter)
|
||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
||||
* DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60))
|
||||
* DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter)
|
||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
||||
* DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +130,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;
|
||||
}
|
||||
|
||||
aimStrain += wiggleBonus * wiggle_multiplier;
|
||||
|
||||
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
|
||||
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier);
|
||||
|
||||
@@ -132,8 +142,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
return aimStrain;
|
||||
}
|
||||
|
||||
private static double calcWideAngleBonus(double angle) => Math.Pow(Math.Sin(3.0 / 4 * (Math.Min(5.0 / 6 * Math.PI, Math.Max(Math.PI / 6, angle)) - Math.PI / 6)), 2);
|
||||
private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(40), double.DegreesToRadians(140));
|
||||
|
||||
private static double calcAcuteAngleBonus(double angle) => 1 - calcWideAngleBonus(angle);
|
||||
private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(140), double.DegreesToRadians(40));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,12 +52,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
var currentObj = (OsuDifficultyHitObject)current.Previous(i);
|
||||
var currentHitObject = (OsuHitObject)(currentObj.BaseObject);
|
||||
|
||||
cumulativeStrainTime += lastObj.StrainTime;
|
||||
|
||||
if (!(currentObj.BaseObject is Spinner))
|
||||
{
|
||||
double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.StackedEndPosition).Length;
|
||||
|
||||
cumulativeStrainTime += lastObj.StrainTime;
|
||||
|
||||
// We want to nerf objects that can be easily seen within the Flashlight circle radius.
|
||||
if (i == 0)
|
||||
smallDistNerf = Math.Min(1.0, jumpDistance / 75.0);
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
@@ -14,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
|
||||
private const double min_speed_bonus = 200; // 200 BPM 1/4th
|
||||
private const double speed_balancing_factor = 40;
|
||||
private const double distance_multiplier = 0.94;
|
||||
private const double distance_multiplier = 0.9;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the difficulty of tapping the current object, based on:
|
||||
@@ -24,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
/// <item><description>and how easily they can be cheesed.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
if (current.BaseObject is Spinner)
|
||||
return 0;
|
||||
@@ -56,6 +60,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
|
||||
double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier;
|
||||
|
||||
if (mods.OfType<OsuModAutopilot>().Any())
|
||||
distanceBonus = 0;
|
||||
|
||||
// Base difficulty with all bonuses
|
||||
double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
@@ -19,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("aim_difficulty")]
|
||||
public double AimDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of <see cref="Slider"/>s weighted by difficulty.
|
||||
/// </summary>
|
||||
[JsonProperty("aim_difficult_slider_count")]
|
||||
public double AimDifficultSliderCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the speed skill.
|
||||
/// </summary>
|
||||
@@ -52,24 +59,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("speed_difficult_strain_count")]
|
||||
public double SpeedDifficultStrainCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("approach_rate")]
|
||||
public double ApproachRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the overall difficulty value, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("overall_difficulty")]
|
||||
public double OverallDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The beatmap's drain rate. This doesn't scale with rate-adjusting mods.
|
||||
/// </summary>
|
||||
@@ -97,8 +86,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
yield return (ATTRIB_ID_AIM, AimDifficulty);
|
||||
yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
|
||||
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
|
||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
|
||||
if (ShouldSerializeFlashlightDifficulty())
|
||||
@@ -109,6 +96,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
yield return (ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT, AimDifficultStrainCount);
|
||||
yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
|
||||
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
|
||||
yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
@@ -117,14 +105,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
AimDifficulty = values[ATTRIB_ID_AIM];
|
||||
SpeedDifficulty = values[ATTRIB_ID_SPEED];
|
||||
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
|
||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
||||
AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT];
|
||||
SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
|
||||
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
|
||||
AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT];
|
||||
DrainRate = onlineInfo.DrainRate;
|
||||
HitCircleCount = onlineInfo.CircleCount;
|
||||
SliderCount = onlineInfo.SliderCount;
|
||||
|
||||
@@ -15,8 +15,6 @@ using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
@@ -24,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
private const double difficulty_multiplier = 0.0675;
|
||||
|
||||
public override int Version => 20241007;
|
||||
public override int Version => 20250306;
|
||||
|
||||
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
@@ -36,20 +34,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
return new OsuDifficultyAttributes { Mods = mods };
|
||||
|
||||
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
|
||||
double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
|
||||
double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
|
||||
double speedNotes = ((Speed)skills[2]).RelevantNoteCount();
|
||||
|
||||
double flashlightRating = 0.0;
|
||||
|
||||
if (mods.Any(h => h is OsuModFlashlight))
|
||||
flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier;
|
||||
var aim = skills.OfType<Aim>().Single(a => a.IncludeSliders);
|
||||
double aimRating = Math.Sqrt(aim.DifficultyValue()) * difficulty_multiplier;
|
||||
double aimDifficultyStrainCount = aim.CountTopWeightedStrains();
|
||||
double difficultSliders = aim.GetDifficultSliders();
|
||||
|
||||
var aimWithoutSliders = skills.OfType<Aim>().Single(a => !a.IncludeSliders);
|
||||
double aimRatingNoSliders = Math.Sqrt(aimWithoutSliders.DifficultyValue()) * difficulty_multiplier;
|
||||
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
|
||||
|
||||
double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountTopWeightedStrains();
|
||||
double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountTopWeightedStrains();
|
||||
var speed = skills.OfType<Speed>().Single();
|
||||
double speedRating = Math.Sqrt(speed.DifficultyValue()) * difficulty_multiplier;
|
||||
double speedNotes = speed.RelevantNoteCount();
|
||||
double speedDifficultyStrainCount = speed.CountTopWeightedStrains();
|
||||
|
||||
var flashlight = skills.OfType<Flashlight>().SingleOrDefault();
|
||||
double flashlightRating = flashlight == null ? 0.0 : Math.Sqrt(flashlight.DifficultyValue()) * difficulty_multiplier;
|
||||
|
||||
if (mods.Any(m => m is OsuModTouchDevice))
|
||||
{
|
||||
@@ -63,6 +63,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
speedRating = 0.0;
|
||||
flashlightRating *= 0.7;
|
||||
}
|
||||
else if (mods.Any(h => h is OsuModAutopilot))
|
||||
{
|
||||
speedRating *= 0.5;
|
||||
aimRating = 0.0;
|
||||
flashlightRating *= 0.4;
|
||||
}
|
||||
|
||||
double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
|
||||
double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);
|
||||
@@ -82,31 +88,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4)
|
||||
: 0;
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
double drainRate = beatmap.Difficulty.DrainRate;
|
||||
|
||||
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
||||
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
||||
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
|
||||
|
||||
HitWindows hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
|
||||
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
|
||||
{
|
||||
StarRating = starRating,
|
||||
Mods = mods,
|
||||
AimDifficulty = aimRating,
|
||||
AimDifficultSliderCount = difficultSliders,
|
||||
SpeedDifficulty = speedRating,
|
||||
SpeedNoteCount = speedNotes,
|
||||
FlashlightDifficulty = flashlightRating,
|
||||
SliderFactor = sliderFactor,
|
||||
AimDifficultStrainCount = aimDifficultyStrainCount,
|
||||
SpeedDifficultStrainCount = speedDifficultyStrainCount,
|
||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
||||
DrainRate = drainRate,
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
HitCircleCount = hitCirclesCount,
|
||||
|
||||
@@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("effective_miss_count")]
|
||||
public double EffectiveMissCount { get; set; }
|
||||
|
||||
[JsonProperty("speed_deviation")]
|
||||
public double? SpeedDeviation { get; set; }
|
||||
|
||||
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
||||
{
|
||||
foreach (var attribute in base.GetAttributesForDisplay())
|
||||
|
||||
@@ -4,9 +4,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
@@ -40,6 +46,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
/// </summary>
|
||||
private double effectiveMissCount;
|
||||
|
||||
private double clockRate;
|
||||
private double greatHitWindow;
|
||||
private double okHitWindow;
|
||||
private double mehHitWindow;
|
||||
private double overallDifficulty;
|
||||
private double approachRate;
|
||||
|
||||
private double? speedDeviation;
|
||||
|
||||
public OsuPerformanceCalculator()
|
||||
: base(new OsuRuleset())
|
||||
{
|
||||
@@ -61,6 +76,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
countSliderTickMiss = score.Statistics.GetValueOrDefault(HitResult.LargeTickMiss);
|
||||
effectiveMissCount = countMiss;
|
||||
|
||||
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
|
||||
|
||||
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
||||
|
||||
var track = new TrackVirtual(10000);
|
||||
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
clockRate = track.Rate;
|
||||
|
||||
HitWindows hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(difficulty.OverallDifficulty);
|
||||
|
||||
greatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
okHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate;
|
||||
mehHitWindow = hitWindows.WindowFor(HitResult.Meh) / clockRate;
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
overallDifficulty = (80 - greatHitWindow) / 6;
|
||||
approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5;
|
||||
|
||||
if (osuAttributes.SliderCount > 0)
|
||||
{
|
||||
if (usingClassicSliderAccuracy)
|
||||
@@ -103,17 +138,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
// https://www.desmos.com/calculator/bc9eybdthb
|
||||
// we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0
|
||||
// this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11)
|
||||
double okMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 1.8) : 1.0);
|
||||
double mehMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 5) : 1.0);
|
||||
double okMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 1.8) : 1.0);
|
||||
double mehMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 5) : 1.0);
|
||||
|
||||
// As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
|
||||
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
|
||||
}
|
||||
|
||||
speedDeviation = calculateSpeedDeviation(osuAttributes);
|
||||
|
||||
double aimValue = computeAimValue(score, osuAttributes);
|
||||
double speedValue = computeSpeedValue(score, osuAttributes);
|
||||
double accuracyValue = computeAccuracyValue(score, osuAttributes);
|
||||
double flashlightValue = computeFlashlightValue(score, osuAttributes);
|
||||
|
||||
double totalValue =
|
||||
Math.Pow(
|
||||
Math.Pow(aimValue, 1.1) +
|
||||
@@ -129,13 +167,40 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
Accuracy = accuracyValue,
|
||||
Flashlight = flashlightValue,
|
||||
EffectiveMissCount = effectiveMissCount,
|
||||
SpeedDeviation = speedDeviation,
|
||||
Total = totalValue
|
||||
};
|
||||
}
|
||||
|
||||
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty);
|
||||
if (score.Mods.Any(h => h is OsuModAutopilot))
|
||||
return 0.0;
|
||||
|
||||
double aimDifficulty = attributes.AimDifficulty;
|
||||
|
||||
if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0)
|
||||
{
|
||||
double estimateImproperlyFollowedDifficultSliders;
|
||||
|
||||
if (usingClassicSliderAccuracy)
|
||||
{
|
||||
// When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders
|
||||
int maximumPossibleDroppedSliders = totalImperfectHits;
|
||||
estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, attributes.AimDifficultSliderCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We add tick misses here since they too mean that the player didn't follow the slider properly
|
||||
// We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
|
||||
estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, attributes.AimDifficultSliderCount);
|
||||
}
|
||||
|
||||
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / attributes.AimDifficultSliderCount, 3) + attributes.SliderFactor;
|
||||
aimDifficulty *= sliderNerfFactor;
|
||||
}
|
||||
|
||||
double aimValue = OsuStrainSkill.DifficultyToPerformance(aimDifficulty);
|
||||
|
||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||
@@ -145,10 +210,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
aimValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount);
|
||||
|
||||
double approachRateFactor = 0.0;
|
||||
if (attributes.ApproachRate > 10.33)
|
||||
approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
|
||||
else if (attributes.ApproachRate < 8.0)
|
||||
approachRateFactor = 0.05 * (8.0 - attributes.ApproachRate);
|
||||
if (approachRate > 10.33)
|
||||
approachRateFactor = 0.3 * (approachRate - 10.33);
|
||||
else if (approachRate < 8.0)
|
||||
approachRateFactor = 0.05 * (8.0 - approachRate);
|
||||
|
||||
if (score.Mods.Any(h => h is OsuModRelax))
|
||||
approachRateFactor = 0.0;
|
||||
@@ -160,43 +225,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
|
||||
{
|
||||
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
||||
aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
|
||||
}
|
||||
|
||||
// We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator.
|
||||
double estimateDifficultSliders = attributes.SliderCount * 0.15;
|
||||
|
||||
if (attributes.SliderCount > 0)
|
||||
{
|
||||
double estimateImproperlyFollowedDifficultSliders;
|
||||
|
||||
if (usingClassicSliderAccuracy)
|
||||
{
|
||||
// When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders
|
||||
int maximumPossibleDroppedSliders = totalImperfectHits;
|
||||
estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We add tick misses here since they too mean that the player didn't follow the slider properly
|
||||
// We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
|
||||
estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, estimateDifficultSliders);
|
||||
}
|
||||
|
||||
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor;
|
||||
aimValue *= sliderNerfFactor;
|
||||
aimValue *= 1.0 + 0.04 * (12.0 - approachRate);
|
||||
}
|
||||
|
||||
aimValue *= accuracy;
|
||||
// It is important to consider accuracy difficulty when scaling with accuracy.
|
||||
aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
|
||||
aimValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
|
||||
|
||||
return aimValue;
|
||||
}
|
||||
|
||||
private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (score.Mods.Any(h => h is OsuModRelax))
|
||||
if (score.Mods.Any(h => h is OsuModRelax) || speedDeviation == null)
|
||||
return 0.0;
|
||||
|
||||
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||
@@ -209,8 +250,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
speedValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount);
|
||||
|
||||
double approachRateFactor = 0.0;
|
||||
if (attributes.ApproachRate > 10.33)
|
||||
approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
|
||||
if (approachRate > 10.33)
|
||||
approachRateFactor = 0.3 * (approachRate - 10.33);
|
||||
|
||||
if (score.Mods.Any(h => h is OsuModAutopilot))
|
||||
approachRateFactor = 0.0;
|
||||
|
||||
speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
|
||||
|
||||
@@ -222,21 +266,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
|
||||
{
|
||||
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
||||
speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
|
||||
speedValue *= 1.0 + 0.04 * (12.0 - approachRate);
|
||||
}
|
||||
|
||||
double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes);
|
||||
speedValue *= speedHighDeviationMultiplier;
|
||||
|
||||
// Calculate accuracy assuming the worst case scenario
|
||||
double relevantTotalDiff = totalHits - attributes.SpeedNoteCount;
|
||||
double relevantTotalDiff = Math.Max(0, totalHits - attributes.SpeedNoteCount);
|
||||
double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
|
||||
double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat));
|
||||
double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk));
|
||||
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
|
||||
|
||||
// Scale the speed value with accuracy and OD.
|
||||
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2);
|
||||
|
||||
// Scale the speed value with # of 50s to punish doubletapping.
|
||||
speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
||||
speedValue *= (0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - overallDifficulty) / 2);
|
||||
|
||||
return speedValue;
|
||||
}
|
||||
@@ -253,7 +297,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
amountHitObjectsWithAccuracy += attributes.SliderCount;
|
||||
|
||||
if (amountHitObjectsWithAccuracy > 0)
|
||||
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
|
||||
betterAccuracyPercentage = ((countGreat - Math.Max(totalHits - amountHitObjectsWithAccuracy, 0)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
|
||||
else
|
||||
betterAccuracyPercentage = 0;
|
||||
|
||||
@@ -263,7 +307,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
// Lots of arbitrary values from testing.
|
||||
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution.
|
||||
double accuracyValue = Math.Pow(1.52163, attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
|
||||
double accuracyValue = Math.Pow(1.52163, overallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
|
||||
|
||||
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
|
||||
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
|
||||
@@ -300,17 +344,117 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
// Scale the flashlight value with accuracy _slightly_.
|
||||
flashlightValue *= 0.5 + accuracy / 2.0;
|
||||
// It is important to also consider accuracy difficulty when doing that.
|
||||
flashlightValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
|
||||
flashlightValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
|
||||
|
||||
return flashlightValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estimates player's deviation on speed notes using <see cref="calculateDeviation"/>, assuming worst-case.
|
||||
/// Treats all speed notes as hit circles.
|
||||
/// </summary>
|
||||
private double? calculateSpeedDeviation(OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (totalSuccessfulHits == 0)
|
||||
return null;
|
||||
|
||||
// Calculate accuracy assuming the worst case scenario
|
||||
double speedNoteCount = attributes.SpeedNoteCount;
|
||||
speedNoteCount += (totalHits - attributes.SpeedNoteCount) * 0.1;
|
||||
|
||||
// Assume worst case: all mistakes were on speed notes
|
||||
double relevantCountMiss = Math.Min(countMiss, speedNoteCount);
|
||||
double relevantCountMeh = Math.Min(countMeh, speedNoteCount - relevantCountMiss);
|
||||
double relevantCountOk = Math.Min(countOk, speedNoteCount - relevantCountMiss - relevantCountMeh);
|
||||
double relevantCountGreat = Math.Max(0, speedNoteCount - relevantCountMiss - relevantCountMeh - relevantCountOk);
|
||||
|
||||
return calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estimates the player's tap deviation based on the OD, given number of greats, oks, mehs and misses,
|
||||
/// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings
|
||||
/// will always return the same deviation. Misses are ignored because they are usually due to misaiming.
|
||||
/// Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution.
|
||||
/// </summary>
|
||||
private double? calculateDeviation(OsuDifficultyAttributes attributes, double relevantCountGreat, double relevantCountOk, double relevantCountMeh, double relevantCountMiss)
|
||||
{
|
||||
if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0)
|
||||
return null;
|
||||
|
||||
double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss;
|
||||
|
||||
// The probability that a player hits a circle is unknown, but we can estimate it to be
|
||||
// the number of greats on circles divided by the number of circles, and then add one
|
||||
// to the number of circles as a bias correction.
|
||||
double n = Math.Max(1, objectCount - relevantCountMiss - relevantCountMeh);
|
||||
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
|
||||
|
||||
// Proportion of greats hit on circles, ignoring misses and 50s.
|
||||
double p = relevantCountGreat / n;
|
||||
|
||||
// We can be 99% confident that p is at least this value.
|
||||
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||
|
||||
// Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed.
|
||||
// Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than:
|
||||
double deviation = greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound));
|
||||
|
||||
double randomValue = Math.Sqrt(2 / Math.PI) * okHitWindow * Math.Exp(-0.5 * Math.Pow(okHitWindow / deviation, 2))
|
||||
/ (deviation * DifficultyCalculationUtils.Erf(okHitWindow / (Math.Sqrt(2) * deviation)));
|
||||
|
||||
deviation *= Math.Sqrt(1 - randomValue);
|
||||
|
||||
// Value deviation approach as greatCount approaches 0
|
||||
double limitValue = okHitWindow / Math.Sqrt(3);
|
||||
|
||||
// If precision is not enough to compute true deviation - use limit value
|
||||
if (pLowerBound == 0 || randomValue >= 1 || deviation > limitValue)
|
||||
deviation = limitValue;
|
||||
|
||||
// Then compute the variance for mehs.
|
||||
double mehVariance = (mehHitWindow * mehHitWindow + okHitWindow * mehHitWindow + okHitWindow * okHitWindow) / 3;
|
||||
|
||||
// Find the total deviation.
|
||||
deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh));
|
||||
|
||||
return deviation;
|
||||
}
|
||||
|
||||
// Calculates multiplier for speed to account for improper tapping based on the deviation and speed difficulty
|
||||
// https://www.desmos.com/calculator/dmogdhzofn
|
||||
private double calculateSpeedHighDeviationNerf(OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (speedDeviation == null)
|
||||
return 0;
|
||||
|
||||
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||
|
||||
// Decides a point where the PP value achieved compared to the speed deviation is assumed to be tapped improperly. Any PP above this point is considered "excess" speed difficulty.
|
||||
// This is used to cause PP above the cutoff to scale logarithmically towards the original speed value thus nerfing the value.
|
||||
double excessSpeedDifficultyCutoff = 100 + 220 * Math.Pow(22 / speedDeviation.Value, 6.5);
|
||||
|
||||
if (speedValue <= excessSpeedDifficultyCutoff)
|
||||
return 1.0;
|
||||
|
||||
const double scale = 50;
|
||||
double adjustedSpeedValue = scale * (Math.Log((speedValue - excessSpeedDifficultyCutoff) / scale + 1) + excessSpeedDifficultyCutoff / scale);
|
||||
|
||||
// 220 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible
|
||||
double lerp = 1 - DifficultyCalculationUtils.ReverseLerp(speedDeviation.Value, 22.0, 27.0);
|
||||
adjustedSpeedValue = double.Lerp(adjustedSpeedValue, speedValue, lerp);
|
||||
|
||||
return adjustedSpeedValue / speedValue;
|
||||
}
|
||||
|
||||
// Miss penalty assumes that a player will miss on the hardest parts of a map,
|
||||
// so we use the amount of relatively difficult sections to adjust miss penalty
|
||||
// to make it more punishing on maps with lower amount of hard sections.
|
||||
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1);
|
||||
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
|
||||
|
||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
||||
private int totalImperfectHits => countOk + countMeh + countMiss;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
@@ -13,19 +16,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// </summary>
|
||||
public class Aim : OsuStrainSkill
|
||||
{
|
||||
public Aim(Mod[] mods, bool withSliders)
|
||||
public readonly bool IncludeSliders;
|
||||
|
||||
public Aim(Mod[] mods, bool includeSliders)
|
||||
: base(mods)
|
||||
{
|
||||
this.withSliders = withSliders;
|
||||
IncludeSliders = includeSliders;
|
||||
}
|
||||
|
||||
private readonly bool withSliders;
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
private double skillMultiplier => 25.18;
|
||||
private double skillMultiplier => 25.6;
|
||||
private double strainDecayBase => 0.15;
|
||||
|
||||
private readonly List<double> sliderStrains = new List<double>();
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
|
||||
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime);
|
||||
@@ -33,9 +38,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
protected override double StrainValueAt(DifficultyHitObject current)
|
||||
{
|
||||
currentStrain *= strainDecay(current.DeltaTime);
|
||||
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier;
|
||||
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplier;
|
||||
|
||||
if (current.BaseObject is Slider)
|
||||
{
|
||||
sliderStrains.Add(currentStrain);
|
||||
}
|
||||
|
||||
return currentStrain;
|
||||
}
|
||||
|
||||
public double GetDifficultSliders()
|
||||
{
|
||||
if (sliderStrains.Count == 0)
|
||||
return 0;
|
||||
|
||||
double maxSliderStrain = sliderStrains.Max();
|
||||
if (maxSliderStrain == 0)
|
||||
return 0;
|
||||
|
||||
return sliderStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// </summary>
|
||||
public class Speed : OsuStrainSkill
|
||||
{
|
||||
private double skillMultiplier => 1.430;
|
||||
private double skillMultiplier => 1.46;
|
||||
private double strainDecayBase => 0.3;
|
||||
|
||||
private double currentStrain;
|
||||
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
protected override double StrainValueAt(DifficultyHitObject current)
|
||||
{
|
||||
currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime);
|
||||
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier;
|
||||
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, Mods) * skillMultiplier;
|
||||
|
||||
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
|
||||
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -76,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
|
||||
if (hasReachedObject && showHitMarkers.Value)
|
||||
{
|
||||
float alpha = Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION, Easing.In);
|
||||
float ringScale = MathHelper.Clamp(Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION / 2, Easing.OutQuint), 0, 1);
|
||||
float ringScale = Math.Clamp(Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION / 2, Easing.OutQuint), 0, 1);
|
||||
|
||||
ring.Scale = new Vector2(1 + 0.1f * ringScale);
|
||||
content.Alpha = 0.9f * (1 - alpha);
|
||||
|
||||
+1
-1
@@ -484,7 +484,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
// Snap the path to the current beat divisor before checking length validity.
|
||||
hitObject.SnapTo(distanceSnapProvider);
|
||||
|
||||
if (!hitObject.Path.HasValidLength)
|
||||
if (!hitObject.Path.HasValidLengthForPlacement)
|
||||
{
|
||||
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
hitObject.Path.ControlPoints[i].Position = oldControlPoints[i];
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
||||
|
||||
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
|
||||
protected override bool IsValidForPlacement => HitObject.Path.HasValidLengthForPlacement;
|
||||
|
||||
public SliderPlacementBlueprint()
|
||||
: base(new Slider())
|
||||
|
||||
@@ -270,14 +270,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
if (adjustVelocity)
|
||||
{
|
||||
proposedVelocity = proposedDistance / oldDuration;
|
||||
proposedDistance = MathHelper.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration);
|
||||
proposedDistance = Math.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration);
|
||||
}
|
||||
else
|
||||
{
|
||||
double minDistance = distanceSnapProvider?.GetBeatSnapDistance() * oldVelocityMultiplier ?? 1;
|
||||
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
|
||||
proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance;
|
||||
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
||||
proposedDistance = Math.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
||||
}
|
||||
|
||||
if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier))
|
||||
@@ -476,7 +476,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
HitObject.SnapTo(distanceSnapProvider);
|
||||
|
||||
// If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted
|
||||
if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength)
|
||||
if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLengthForPlacement)
|
||||
{
|
||||
placementHandler?.Delete(HitObject);
|
||||
return;
|
||||
@@ -626,7 +626,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
{
|
||||
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && (IsSelected || DrawableObject.Body.Alpha > 0))
|
||||
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && (IsSelected || DrawableObject.Body.Alpha > 0 || DrawableObject.HeadCircle.Alpha > 0))
|
||||
return true;
|
||||
|
||||
if (ControlPointVisualiser == null)
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
public BindableFloat Spacing { get; } = new BindableFloat(4f)
|
||||
{
|
||||
MinValue = 4f,
|
||||
MaxValue = 128f,
|
||||
MaxValue = 256f,
|
||||
Precision = 0.01f,
|
||||
};
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
@@ -351,6 +352,35 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
// Why is this logic here and not in `OsuSelectionHandler`?
|
||||
// Because we only want to handle this toggle after all other right-click handling completes.
|
||||
//
|
||||
// Consider that input is handled from the most nested child first:
|
||||
//
|
||||
// ComposeScreen
|
||||
// |- OsuContextMenuContainer // right click for context
|
||||
// |- TimelineBlueprintContainer
|
||||
// |- TimelineSelectionHandler
|
||||
// |- (Osu)HitObjectComposer // right click for toggle new combo
|
||||
// |- (Osu)EditorBlueprintContainer // right click for select
|
||||
// |- (Osu)EditorSelectionHandler // right click for delete
|
||||
if (e.Button == MouseButton.Right)
|
||||
{
|
||||
var osuSelectionHandler = (OsuSelectionHandler)BlueprintContainer.SelectionHandler;
|
||||
|
||||
if (!osuSelectionHandler.SelectedItems.Any())
|
||||
{
|
||||
osuSelectionHandler.SelectionNewComboState.Value =
|
||||
osuSelectionHandler.SelectionNewComboState.Value == TernaryState.False ? TernaryState.True : TernaryState.False;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
|
||||
@@ -180,7 +180,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
||||
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||
|
||||
if (xInBounds && yInBounds && slider.Path.HasValidLength)
|
||||
if (xInBounds && yInBounds && slider.Path.HasValidLengthForPlacement)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
@@ -263,12 +263,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
case Axes.X:
|
||||
(sLowerBound, sUpperBound) = computeBounds(lowerBounds - b, upperBounds - b, a);
|
||||
s.X = MathHelper.Clamp(s.X, sLowerBound, sUpperBound);
|
||||
s.X = Math.Clamp(s.X, sLowerBound, sUpperBound);
|
||||
break;
|
||||
|
||||
case Axes.Y:
|
||||
(sLowerBound, sUpperBound) = computeBounds(lowerBounds - a, upperBounds - a, b);
|
||||
s.Y = MathHelper.Clamp(s.Y, sLowerBound, sUpperBound);
|
||||
s.Y = Math.Clamp(s.Y, sLowerBound, sUpperBound);
|
||||
break;
|
||||
|
||||
case Axes.Both:
|
||||
@@ -276,11 +276,11 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
// Therefore the ratio s.X / s.Y will be maintained
|
||||
(sLowerBound, sUpperBound) = computeBounds(lowerBounds, upperBounds, a * s.X + b * s.Y);
|
||||
s.X = s.X < 0
|
||||
? MathHelper.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound)
|
||||
: MathHelper.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound);
|
||||
? Math.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound)
|
||||
: Math.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound);
|
||||
s.Y = s.Y < 0
|
||||
? MathHelper.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound)
|
||||
: MathHelper.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound);
|
||||
? Math.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound)
|
||||
: Math.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
@@ -127,8 +128,11 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
if (relativeCheckbox.Current.Value)
|
||||
{
|
||||
(xBindable.MinValue, xBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.X, OsuPlayfield.BASE_SIZE.X - initialSurroundingQuad.BottomRight.X);
|
||||
(yBindable.MinValue, yBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - initialSurroundingQuad.BottomRight.Y);
|
||||
xBindable.MinValue = 0 - Math.Max(initialSurroundingQuad.TopLeft.X, 0);
|
||||
xBindable.MaxValue = OsuPlayfield.BASE_SIZE.X - Math.Min(initialSurroundingQuad.BottomRight.X, OsuPlayfield.BASE_SIZE.X);
|
||||
|
||||
yBindable.MinValue = 0 - Math.Max(initialSurroundingQuad.TopLeft.Y, 0);
|
||||
yBindable.MaxValue = OsuPlayfield.BASE_SIZE.Y - Math.Min(initialSurroundingQuad.BottomRight.Y, OsuPlayfield.BASE_SIZE.Y);
|
||||
|
||||
xBindable.Default = yBindable.Default = 0;
|
||||
|
||||
@@ -146,8 +150,21 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
var quadRelativeToPosition = new RectangleF(initialSurroundingQuad.Location - initialPosition, initialSurroundingQuad.Size);
|
||||
|
||||
(xBindable.MinValue, xBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.X, OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X);
|
||||
(yBindable.MinValue, yBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y);
|
||||
if (initialSurroundingQuad.Width < OsuPlayfield.BASE_SIZE.X)
|
||||
{
|
||||
xBindable.MinValue = 0 - quadRelativeToPosition.TopLeft.X;
|
||||
xBindable.MaxValue = OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X;
|
||||
}
|
||||
else
|
||||
xBindable.MinValue = xBindable.MaxValue = initialPosition.X;
|
||||
|
||||
if (initialSurroundingQuad.Height < OsuPlayfield.BASE_SIZE.Y)
|
||||
{
|
||||
yBindable.MinValue = 0 - quadRelativeToPosition.TopLeft.Y;
|
||||
yBindable.MaxValue = OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y;
|
||||
}
|
||||
else
|
||||
yBindable.MinValue = yBindable.MaxValue = initialPosition.Y;
|
||||
|
||||
xBindable.Default = initialPosition.X;
|
||||
yBindable.Default = initialPosition.Y;
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osu.Game.Tests.Visual;
|
||||
@@ -31,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor
|
||||
AddStep("hover over first hit", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<DrawableHit>().ElementAt(1)));
|
||||
AddStep("hover over second hit", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<DrawableHit>().ElementAt(0)));
|
||||
AddStep("right click", () => InputManager.Click(MouseButton.Right));
|
||||
AddUntilStep("context menu open", () => Editor.ChildrenOfType<OsuContextMenu>().Any(menu => menu.State == MenuState.Open));
|
||||
AddUntilStep("second hit deleted", () => Editor.ChildrenOfType<DrawableHit>().Count(), () => Is.EqualTo(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
||||
|
||||
[TestCase(3.0920212594351191d, 200, "diffcalc-test")]
|
||||
[TestCase(3.0920212594351191d, 200, "diffcalc-test-strong")]
|
||||
[TestCase(3.305554470092722d, 200, "diffcalc-test")]
|
||||
[TestCase(3.305554470092722d, 200, "diffcalc-test-strong")]
|
||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||
|
||||
[TestCase(4.0789820318081444d, 200, "diffcalc-test")]
|
||||
[TestCase(4.0789820318081444d, 200, "diffcalc-test-strong")]
|
||||
[TestCase(4.4472572672057815d, 200, "diffcalc-test")]
|
||||
[TestCase(4.4472572672057815d, 200, "diffcalc-test-strong")]
|
||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
|
||||
|
||||
|
||||
@@ -10,45 +10,82 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
{
|
||||
public class ColourEvaluator
|
||||
public static class ColourEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluate the difficulty of the first note of a <see cref="MonoStreak"/>.
|
||||
/// Calculates a consistency penalty based on the number of consecutive consistent intervals,
|
||||
/// considering the delta time between each colour sequence.
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(MonoStreak monoStreak)
|
||||
/// <param name="hitObject">The current hitObject to consider.</param>
|
||||
/// <param name="threshold"> The allowable margin of error for determining whether ratios are consistent.</param>
|
||||
/// <param name="maxObjectsToCheck">The maximum objects to check per count of consistent ratio.</param>
|
||||
private static double consistentRatioPenalty(TaikoDifficultyHitObject hitObject, double threshold = 0.01, int maxObjectsToCheck = 64)
|
||||
{
|
||||
return DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5;
|
||||
int consistentRatioCount = 0;
|
||||
double totalRatioCount = 0.0;
|
||||
|
||||
TaikoDifficultyHitObject current = hitObject;
|
||||
|
||||
for (int i = 0; i < maxObjectsToCheck; i++)
|
||||
{
|
||||
// Break if there is no valid previous object
|
||||
if (current.Index <= 1)
|
||||
break;
|
||||
|
||||
var previousHitObject = (TaikoDifficultyHitObject)current.Previous(1);
|
||||
|
||||
double currentRatio = current.RhythmData.Ratio;
|
||||
double previousRatio = previousHitObject.RhythmData.Ratio;
|
||||
|
||||
// A consistent interval is defined as the percentage difference between the two rhythmic ratios with the margin of error.
|
||||
if (Math.Abs(1 - currentRatio / previousRatio) <= threshold)
|
||||
{
|
||||
consistentRatioCount++;
|
||||
totalRatioCount += currentRatio;
|
||||
break;
|
||||
}
|
||||
|
||||
// Move to the previous object
|
||||
current = previousHitObject;
|
||||
}
|
||||
|
||||
// Ensure no division by zero
|
||||
double ratioPenalty = 1 - totalRatioCount / (consistentRatioCount + 1) * 0.80;
|
||||
|
||||
return ratioPenalty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluate the difficulty of the first note of a <see cref="AlternatingMonoPattern"/>.
|
||||
/// Evaluate the difficulty of the first hitobject within a colour streak.
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern)
|
||||
{
|
||||
return DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * EvaluateDifficultyOf(alternatingMonoPattern.Parent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluate the difficulty of the first note of a <see cref="RepeatingHitPatterns"/>.
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern)
|
||||
{
|
||||
return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
|
||||
}
|
||||
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject)
|
||||
{
|
||||
TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour;
|
||||
var taikoObject = (TaikoDifficultyHitObject)hitObject;
|
||||
TaikoColourData colourData = taikoObject.ColourData;
|
||||
double difficulty = 0.0d;
|
||||
|
||||
if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak
|
||||
difficulty += EvaluateDifficultyOf(colour.MonoStreak);
|
||||
if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern
|
||||
difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern);
|
||||
if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
|
||||
difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern);
|
||||
if (colourData.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak
|
||||
difficulty += evaluateMonoStreakDifficulty(colourData.MonoStreak);
|
||||
|
||||
if (colourData.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern
|
||||
difficulty += evaluateAlternatingMonoPatternDifficulty(colourData.AlternatingMonoPattern);
|
||||
|
||||
if (colourData.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
|
||||
difficulty += evaluateRepeatingHitPatternsDifficulty(colourData.RepeatingHitPattern);
|
||||
|
||||
double consistencyPenalty = consistentRatioPenalty(taikoObject);
|
||||
difficulty *= consistencyPenalty;
|
||||
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
private static double evaluateMonoStreakDifficulty(MonoStreak monoStreak) =>
|
||||
DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * evaluateAlternatingMonoPatternDifficulty(monoStreak.Parent) * 0.5;
|
||||
|
||||
private static double evaluateAlternatingMonoPatternDifficulty(AlternatingMonoPattern alternatingMonoPattern) =>
|
||||
DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * evaluateRepeatingHitPatternsDifficulty(alternatingMonoPattern.Parent);
|
||||
|
||||
private static double evaluateRepeatingHitPatternsDifficulty(RepeatingHitPatterns repeatingHitPattern) =>
|
||||
2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
{
|
||||
public static class ReadingEvaluator
|
||||
{
|
||||
private readonly struct VelocityRange
|
||||
{
|
||||
public double Min { get; }
|
||||
public double Max { get; }
|
||||
public double Center => (Max + Min) / 2;
|
||||
public double Range => Max - Min;
|
||||
|
||||
public VelocityRange(double min, double max)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the influence of higher slider velocities on hitobject difficulty.
|
||||
/// The bonus is determined based on the EffectiveBPM, shifting within a defined range
|
||||
/// between the upper and lower boundaries to reflect how increased slider velocity impacts difficulty.
|
||||
/// </summary>
|
||||
/// <param name="noteObject">The hit object to evaluate.</param>
|
||||
/// <returns>The reading difficulty value for the given hit object.</returns>
|
||||
public static double EvaluateDifficultyOf(TaikoDifficultyHitObject noteObject)
|
||||
{
|
||||
var highVelocity = new VelocityRange(480, 640);
|
||||
var midVelocity = new VelocityRange(360, 480);
|
||||
|
||||
// Apply a cap to prevent outlier values on maps that exceed the editor's parameters.
|
||||
double effectiveBPM = Math.Max(1.0, noteObject.EffectiveBPM);
|
||||
|
||||
double midVelocityDifficulty = 0.5 * DifficultyCalculationUtils.Logistic(effectiveBPM, midVelocity.Center, 1.0 / (midVelocity.Range / 10));
|
||||
|
||||
// Expected DeltaTime is the DeltaTime this note would need to be spaced equally to a base slider velocity 1/4 note.
|
||||
double expectedDeltaTime = 21000.0 / effectiveBPM;
|
||||
double objectDensity = expectedDeltaTime / Math.Max(1.0, noteObject.DeltaTime);
|
||||
|
||||
// High density is penalised at high velocity as it is generally considered easier to read. See https://www.desmos.com/calculator/u63f3ntdsi
|
||||
double densityPenalty = DifficultyCalculationUtils.Logistic(objectDensity, 0.925, 15);
|
||||
|
||||
double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty)
|
||||
* DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10));
|
||||
|
||||
return midVelocityDifficulty + highVelocityDifficulty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
{
|
||||
public class RhythmEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluate the difficulty of a hitobject considering its interval change.
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow)
|
||||
{
|
||||
TaikoRhythmData rhythmData = ((TaikoDifficultyHitObject)hitObject).RhythmData;
|
||||
double difficulty = 0.0d;
|
||||
|
||||
double sameRhythm = 0;
|
||||
double samePattern = 0;
|
||||
double intervalPenalty = 0;
|
||||
|
||||
if (rhythmData.SameRhythmGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmGroupedHitObjects
|
||||
{
|
||||
sameRhythm += 10.0 * evaluateDifficultyOf(rhythmData.SameRhythmGroupedHitObjects, hitWindow);
|
||||
intervalPenalty = repeatedIntervalPenalty(rhythmData.SameRhythmGroupedHitObjects, hitWindow);
|
||||
}
|
||||
|
||||
if (rhythmData.SamePatternsGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SamePatternsGroupedHitObjects
|
||||
samePattern += 1.15 * ratioDifficulty(rhythmData.SamePatternsGroupedHitObjects.IntervalRatio);
|
||||
|
||||
difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty;
|
||||
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
private static double evaluateDifficultyOf(SameRhythmHitObjectGrouping sameRhythmGroupedHitObjects, double hitWindow)
|
||||
{
|
||||
double intervalDifficulty = ratioDifficulty(sameRhythmGroupedHitObjects.HitObjectIntervalRatio);
|
||||
double? previousInterval = sameRhythmGroupedHitObjects.Previous?.HitObjectInterval;
|
||||
|
||||
intervalDifficulty *= repeatedIntervalPenalty(sameRhythmGroupedHitObjects, hitWindow);
|
||||
|
||||
// If a previous interval exists and there are multiple hit objects in the sequence:
|
||||
if (previousInterval != null && sameRhythmGroupedHitObjects.HitObjects.Count > 1)
|
||||
{
|
||||
double expectedDurationFromPrevious = (double)previousInterval * sameRhythmGroupedHitObjects.HitObjects.Count;
|
||||
double durationDifference = sameRhythmGroupedHitObjects.Duration - expectedDurationFromPrevious;
|
||||
|
||||
if (durationDifference > 0)
|
||||
{
|
||||
intervalDifficulty *= DifficultyCalculationUtils.Logistic(
|
||||
durationDifference / hitWindow,
|
||||
midpointOffset: 0.7,
|
||||
multiplier: 1.0,
|
||||
maxValue: 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Penalise patterns that can be hit within a single hit window.
|
||||
intervalDifficulty *= DifficultyCalculationUtils.Logistic(
|
||||
sameRhythmGroupedHitObjects.Duration / hitWindow,
|
||||
midpointOffset: 0.6,
|
||||
multiplier: 1,
|
||||
maxValue: 1);
|
||||
|
||||
return Math.Pow(intervalDifficulty, 0.75);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the changes in hit object intervals is consistent based on a given threshold.
|
||||
/// </summary>
|
||||
private static double repeatedIntervalPenalty(SameRhythmHitObjectGrouping sameRhythmGroupedHitObjects, double hitWindow, double threshold = 0.1)
|
||||
{
|
||||
double longIntervalPenalty = sameInterval(sameRhythmGroupedHitObjects, 3);
|
||||
|
||||
double shortIntervalPenalty = sameRhythmGroupedHitObjects.HitObjects.Count < 6
|
||||
? sameInterval(sameRhythmGroupedHitObjects, 4)
|
||||
: 1.0; // Returns a non-penalty if there are 6 or more notes within an interval.
|
||||
|
||||
// The duration penalty is based on hit object duration relative to hitWindow.
|
||||
double durationPenalty = Math.Max(1 - sameRhythmGroupedHitObjects.Duration * 2 / hitWindow, 0.5);
|
||||
|
||||
return Math.Min(longIntervalPenalty, shortIntervalPenalty) * durationPenalty;
|
||||
|
||||
double sameInterval(SameRhythmHitObjectGrouping startObject, int intervalCount)
|
||||
{
|
||||
List<double?> intervals = new List<double?>();
|
||||
var currentObject = startObject;
|
||||
|
||||
for (int i = 0; i < intervalCount && currentObject != null; i++)
|
||||
{
|
||||
intervals.Add(currentObject.HitObjectInterval);
|
||||
currentObject = currentObject.Previous;
|
||||
}
|
||||
|
||||
intervals.RemoveAll(interval => interval == null);
|
||||
|
||||
if (intervals.Count < intervalCount)
|
||||
return 1.0; // No penalty if there aren't enough valid intervals.
|
||||
|
||||
for (int i = 0; i < intervals.Count; i++)
|
||||
{
|
||||
for (int j = i + 1; j < intervals.Count; j++)
|
||||
{
|
||||
double ratio = intervals[i]!.Value / intervals[j]!.Value;
|
||||
if (Math.Abs(1 - ratio) <= threshold) // If any two intervals are similar, apply a penalty.
|
||||
return 0.80;
|
||||
}
|
||||
}
|
||||
|
||||
return 1.0; // No penalty if all intervals are different.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses.
|
||||
/// </summary>
|
||||
private static double ratioDifficulty(double ratio, int terms = 8)
|
||||
{
|
||||
double difficulty = 0;
|
||||
|
||||
// Validate the ratio by ensuring it is a normal number in cases where maps breach regular mapping conditions.
|
||||
ratio = double.IsNormal(ratio) ? ratio : 0;
|
||||
|
||||
for (int i = 1; i <= terms; ++i)
|
||||
{
|
||||
difficulty += termPenalty(ratio, i, 4, 1);
|
||||
}
|
||||
|
||||
difficulty += terms / (1 + ratio);
|
||||
|
||||
// Give bonus to near-1 ratios
|
||||
difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5);
|
||||
|
||||
// Penalize ratios that are VERY near 1
|
||||
difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.3);
|
||||
|
||||
difficulty = Math.Max(difficulty, 0);
|
||||
difficulty /= Math.Sqrt(8);
|
||||
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier for a given denominator term.
|
||||
/// </summary>
|
||||
private static double termPenalty(double ratio, int denominator, double power, double multiplier) =>
|
||||
-multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power);
|
||||
}
|
||||
}
|
||||
@@ -8,43 +8,8 @@ using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
{
|
||||
public class StaminaEvaluator
|
||||
public static class StaminaEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies a speed bonus dependent on the time since the last hit performed using this finger.
|
||||
/// </summary>
|
||||
/// <param name="interval">The interval between the current and previous note hit using the same finger.</param>
|
||||
private static double speedBonus(double interval)
|
||||
{
|
||||
// Interval is capped at a very small value to prevent infinite values.
|
||||
interval = Math.Max(interval, 1);
|
||||
|
||||
return 30 / interval;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the number of fingers available to hit the current <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// Any mono notes that is more than 300ms apart from a colour change will be considered to have more than 2
|
||||
/// fingers available, since players can hit the same key with multiple fingers.
|
||||
/// </summary>
|
||||
private static int availableFingersFor(TaikoDifficultyHitObject hitObject)
|
||||
{
|
||||
DifficultyHitObject? previousColourChange = hitObject.Colour.PreviousColourChange;
|
||||
DifficultyHitObject? nextColourChange = hitObject.Colour.NextColourChange;
|
||||
|
||||
if (previousColourChange != null && hitObject.StartTime - previousColourChange.StartTime < 300)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (nextColourChange != null && nextColourChange.StartTime - hitObject.StartTime < 300)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the
|
||||
/// maximum possible interval between two hits using the same key, by alternating available fingers for each colour.
|
||||
@@ -59,17 +24,51 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
// Find the previous hit object hit by the current finger, which is n notes prior, n being the number of
|
||||
// available fingers.
|
||||
TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current;
|
||||
TaikoDifficultyHitObject? keyPrevious = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1);
|
||||
|
||||
if (keyPrevious == null)
|
||||
{
|
||||
// There is no previous hit object hit by the current finger
|
||||
return 0.0;
|
||||
}
|
||||
TaikoDifficultyHitObject? taikoPrevious = current.Previous(1) as TaikoDifficultyHitObject;
|
||||
TaikoDifficultyHitObject? previousMono = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1);
|
||||
|
||||
double objectStrain = 0.5; // Add a base strain to all objects
|
||||
objectStrain += speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime);
|
||||
if (taikoPrevious == null) return objectStrain;
|
||||
|
||||
if (previousMono != null)
|
||||
objectStrain += speedBonus(taikoCurrent.StartTime - previousMono.StartTime) + 0.5 * speedBonus(taikoCurrent.StartTime - taikoPrevious.StartTime);
|
||||
|
||||
return objectStrain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a speed bonus dependent on the time since the last hit performed using this finger.
|
||||
/// </summary>
|
||||
/// <param name="interval">The interval between the current and previous note hit using the same finger.</param>
|
||||
private static double speedBonus(double interval)
|
||||
{
|
||||
// Interval is capped at a very small value to prevent infinite values.
|
||||
interval = Math.Max(interval, 1);
|
||||
|
||||
return 20 / interval;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the number of fingers available to hit the current <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// Any mono notes that is more than 300ms apart from a colour change will be considered to have more than 2
|
||||
/// fingers available, since players can hit the same key with multiple fingers.
|
||||
/// </summary>
|
||||
private static int availableFingersFor(TaikoDifficultyHitObject hitObject)
|
||||
{
|
||||
DifficultyHitObject? previousColourChange = hitObject.ColourData.PreviousColourChange;
|
||||
DifficultyHitObject? nextColourChange = hitObject.ColourData.NextColourChange;
|
||||
|
||||
if (previousColourChange != null && hitObject.StartTime - previousColourChange.StartTime < 300)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (nextColourChange != null && nextColourChange.StartTime - hitObject.StartTime < 300)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
|
||||
/// <summary>
|
||||
/// Stores colour compression information for a <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
public class TaikoDifficultyHitObjectColour
|
||||
public class TaikoColourData
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="MonoStreak"/> that encodes this note.
|
||||
+5
-5
@@ -14,8 +14,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
|
||||
public static class TaikoColourDifficultyPreprocessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes and encodes a list of <see cref="TaikoDifficultyHitObject"/>s into a list of <see cref="TaikoDifficultyHitObjectColour"/>s,
|
||||
/// assigning the appropriate <see cref="TaikoDifficultyHitObjectColour"/>s to each <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// Processes and encodes a list of <see cref="TaikoDifficultyHitObject"/>s into a list of <see cref="TaikoColourData"/>s,
|
||||
/// assigning the appropriate <see cref="TaikoColourData"/>s to each <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
public static void ProcessAndAssign(List<DifficultyHitObject> hitObjects)
|
||||
{
|
||||
@@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
|
||||
|
||||
foreach (var hitObject in monoStreak.HitObjects)
|
||||
{
|
||||
hitObject.Colour.RepeatingHitPattern = repeatingHitPattern;
|
||||
hitObject.Colour.AlternatingMonoPattern = monoPattern;
|
||||
hitObject.Colour.MonoStreak = monoStreak;
|
||||
hitObject.ColourData.RepeatingHitPattern = repeatingHitPattern;
|
||||
hitObject.ColourData.AlternatingMonoPattern = monoPattern;
|
||||
hitObject.ColourData.MonoStreak = monoStreak;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents <see cref="SameRhythmHitObjectGrouping"/> grouped by their <see cref="SameRhythmHitObjectGrouping.StartTime"/>'s interval.
|
||||
/// </summary>
|
||||
public class SamePatternsGroupedHitObjects
|
||||
{
|
||||
public IReadOnlyList<SameRhythmHitObjectGrouping> Groups { get; }
|
||||
|
||||
public SamePatternsGroupedHitObjects? Previous { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SameRhythmHitObjectGrouping.Interval"/> between groups <see cref="SameRhythmHitObjectGrouping"/>.
|
||||
/// If there is only one group, this will have the value of the first group's <see cref="SameRhythmHitObjectGrouping.Interval"/>.
|
||||
/// </summary>
|
||||
public double GroupInterval => Groups.Count > 1 ? Groups[1].Interval : Groups[0].Interval;
|
||||
|
||||
/// <summary>
|
||||
/// The ratio of <see cref="GroupInterval"/> between this and the previous <see cref="SamePatternsGroupedHitObjects"/>. In the
|
||||
/// case where there is no previous <see cref="SamePatternsGroupedHitObjects"/>, this will have a value of 1.
|
||||
/// </summary>
|
||||
public double IntervalRatio => GroupInterval / Previous?.GroupInterval ?? 1.0d;
|
||||
|
||||
public TaikoDifficultyHitObject FirstHitObject => Groups[0].FirstHitObject;
|
||||
|
||||
public IEnumerable<TaikoDifficultyHitObject> AllHitObjects => Groups.SelectMany(hitObject => hitObject.HitObjects);
|
||||
|
||||
public SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List<SameRhythmHitObjectGrouping> groups)
|
||||
{
|
||||
Previous = previous;
|
||||
Groups = groups;
|
||||
}
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a group of <see cref="TaikoDifficultyHitObject"/>s with no rhythm variation.
|
||||
/// </summary>
|
||||
public class SameRhythmHitObjectGrouping : IHasInterval
|
||||
{
|
||||
public readonly List<TaikoDifficultyHitObject> HitObjects;
|
||||
|
||||
public TaikoDifficultyHitObject FirstHitObject => HitObjects[0];
|
||||
|
||||
public readonly SameRhythmHitObjectGrouping? Previous;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DifficultyHitObject.StartTime"/> of the first hit object.
|
||||
/// </summary>
|
||||
public double StartTime => HitObjects[0].StartTime;
|
||||
|
||||
/// <summary>
|
||||
/// The interval between the first and final hit object within this group.
|
||||
/// </summary>
|
||||
public double Duration => HitObjects[^1].StartTime - HitObjects[0].StartTime;
|
||||
|
||||
/// <summary>
|
||||
/// The interval in ms of each hit object in this <see cref="SameRhythmHitObjectGrouping"/>. This is only defined if there is
|
||||
/// more than two hit objects in this <see cref="SameRhythmHitObjectGrouping"/>.
|
||||
/// </summary>
|
||||
public readonly double? HitObjectInterval;
|
||||
|
||||
/// <summary>
|
||||
/// The ratio of <see cref="HitObjectInterval"/> between this and the previous <see cref="SameRhythmHitObjectGrouping"/>. In the
|
||||
/// case where one or both of the <see cref="HitObjectInterval"/> is undefined, this will have a value of 1.
|
||||
/// </summary>
|
||||
public readonly double HitObjectIntervalRatio;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public double Interval { get; }
|
||||
|
||||
public SameRhythmHitObjectGrouping(SameRhythmHitObjectGrouping? previous, List<TaikoDifficultyHitObject> hitObjects)
|
||||
{
|
||||
Previous = previous;
|
||||
HitObjects = hitObjects;
|
||||
|
||||
// Calculate the average interval between hitobjects, or null if there are fewer than two
|
||||
HitObjectInterval = HitObjects.Count < 2 ? null : Duration / (HitObjects.Count - 1);
|
||||
|
||||
// Calculate the ratio between this group's interval and the previous group's interval
|
||||
HitObjectIntervalRatio = Previous?.HitObjectInterval != null && HitObjectInterval != null
|
||||
? HitObjectInterval.Value / Previous.HitObjectInterval.Value
|
||||
: 1;
|
||||
|
||||
// Calculate the interval from the previous group's start time
|
||||
Interval = Previous != null ? StartTime - Previous.StartTime : double.PositiveInfinity;
|
||||
}
|
||||
}
|
||||
}
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a rhythm change in a taiko map.
|
||||
/// </summary>
|
||||
public class TaikoDifficultyHitObjectRhythm
|
||||
{
|
||||
/// <summary>
|
||||
/// The difficulty multiplier associated with this rhythm change.
|
||||
/// </summary>
|
||||
public readonly double Difficulty;
|
||||
|
||||
/// <summary>
|
||||
/// The ratio of current <see cref="osu.Game.Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/>
|
||||
/// to previous <see cref="osu.Game.Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/> for the rhythm change.
|
||||
/// A <see cref="Ratio"/> above 1 indicates a slow-down; a <see cref="Ratio"/> below 1 indicates a speed-up.
|
||||
/// </summary>
|
||||
public readonly double Ratio;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an object representing a rhythm change.
|
||||
/// </summary>
|
||||
/// <param name="numerator">The numerator for <see cref="Ratio"/>.</param>
|
||||
/// <param name="denominator">The denominator for <see cref="Ratio"/></param>
|
||||
/// <param name="difficulty">The difficulty multiplier associated with this rhythm change.</param>
|
||||
public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty)
|
||||
{
|
||||
Ratio = numerator / (double)denominator;
|
||||
Difficulty = difficulty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores rhythm data for a <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
public class TaikoRhythmData
|
||||
{
|
||||
/// <summary>
|
||||
/// The group of hit objects with consistent rhythm that this object belongs to.
|
||||
/// </summary>
|
||||
public SameRhythmHitObjectGrouping? SameRhythmGroupedHitObjects;
|
||||
|
||||
/// <summary>
|
||||
/// The larger pattern of rhythm groups that this object is part of.
|
||||
/// </summary>
|
||||
public SamePatternsGroupedHitObjects? SamePatternsGroupedHitObjects;
|
||||
|
||||
/// <summary>
|
||||
/// The ratio of current <see cref="Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/>
|
||||
/// to previous <see cref="Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/> for the rhythm change.
|
||||
/// A <see cref="Ratio"/> above 1 indicates a slow-down; a <see cref="Ratio"/> below 1 indicates a speed-up.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is snapped to the closest matching <see cref="common_ratios"/>.
|
||||
/// </remarks>
|
||||
public readonly double Ratio;
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a new instance of <see cref="TaikoRhythmData"/>s,
|
||||
/// calculating the closest rhythm change and its associated difficulty for the current hit object.
|
||||
/// </summary>
|
||||
/// <param name="current">The current <see cref="TaikoDifficultyHitObject"/> being processed.</param>
|
||||
public TaikoRhythmData(TaikoDifficultyHitObject current)
|
||||
{
|
||||
var previous = current.Previous(0);
|
||||
|
||||
if (previous == null)
|
||||
{
|
||||
Ratio = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
double actualRatio = current.DeltaTime / previous.DeltaTime;
|
||||
double closestRatio = common_ratios.MinBy(r => Math.Abs(r - actualRatio));
|
||||
|
||||
Ratio = closestRatio;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The general guidelines for the values are:
|
||||
/// <list type="bullet">
|
||||
/// <item>rhythm changes with ratio closer to 1 (that are <i>not</i> 1) are harder to play,</item>
|
||||
/// <item>speeding up is <i>generally</i> harder than slowing down (with exceptions of rhythm changes requiring a hand switch).</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
private static readonly double[] common_ratios =
|
||||
[
|
||||
1.0 / 1,
|
||||
2.0 / 1,
|
||||
1.0 / 2,
|
||||
3.0 / 1,
|
||||
1.0 / 3,
|
||||
3.0 / 2,
|
||||
2.0 / 3,
|
||||
5.0 / 4,
|
||||
4.0 / 5
|
||||
];
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
|
||||
{
|
||||
public static class TaikoRhythmDifficultyPreprocessor
|
||||
{
|
||||
public static void ProcessAndAssign(List<TaikoDifficultyHitObject> hitObjects)
|
||||
{
|
||||
var rhythmGroups = createSameRhythmGroupedHitObjects(hitObjects);
|
||||
|
||||
foreach (var rhythmGroup in rhythmGroups)
|
||||
{
|
||||
foreach (var hitObject in rhythmGroup.HitObjects)
|
||||
hitObject.RhythmData.SameRhythmGroupedHitObjects = rhythmGroup;
|
||||
}
|
||||
|
||||
var patternGroups = createSamePatternGroupedHitObjects(rhythmGroups);
|
||||
|
||||
foreach (var patternGroup in patternGroups)
|
||||
{
|
||||
foreach (var hitObject in patternGroup.AllHitObjects)
|
||||
hitObject.RhythmData.SamePatternsGroupedHitObjects = patternGroup;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<SameRhythmHitObjectGrouping> createSameRhythmGroupedHitObjects(List<TaikoDifficultyHitObject> hitObjects)
|
||||
{
|
||||
var rhythmGroups = new List<SameRhythmHitObjectGrouping>();
|
||||
|
||||
foreach (var grouped in IntervalGroupingUtils.GroupByInterval(hitObjects))
|
||||
rhythmGroups.Add(new SameRhythmHitObjectGrouping(rhythmGroups.LastOrDefault(), grouped));
|
||||
|
||||
return rhythmGroups;
|
||||
}
|
||||
|
||||
private static List<SamePatternsGroupedHitObjects> createSamePatternGroupedHitObjects(List<SameRhythmHitObjectGrouping> rhythmGroups)
|
||||
{
|
||||
var patternGroups = new List<SamePatternsGroupedHitObjects>();
|
||||
|
||||
foreach (var grouped in IntervalGroupingUtils.GroupByInterval(rhythmGroups))
|
||||
patternGroups.Add(new SamePatternsGroupedHitObjects(patternGroups.LastOrDefault(), grouped));
|
||||
|
||||
return patternGroups;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single hit object in taiko difficulty calculation.
|
||||
/// </summary>
|
||||
public class TaikoDifficultyHitObject : DifficultyHitObject
|
||||
public class TaikoDifficultyHitObject : DifficultyHitObject, IHasInterval
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of all <see cref="TaikoDifficultyHitObject"/> of the same colour as this <see cref="TaikoDifficultyHitObject"/> in the beatmap.
|
||||
@@ -38,98 +40,89 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||
public readonly int NoteIndex;
|
||||
|
||||
/// <summary>
|
||||
/// The rhythm required to hit this hit object.
|
||||
/// Rhythm data used by <see cref="RhythmEvaluator"/>.
|
||||
/// This is populated via <see cref="TaikoRhythmDifficultyPreprocessor"/>.
|
||||
/// </summary>
|
||||
public readonly TaikoDifficultyHitObjectRhythm Rhythm;
|
||||
public readonly TaikoRhythmData RhythmData;
|
||||
|
||||
/// <summary>
|
||||
/// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used
|
||||
/// by other skills in the future.
|
||||
/// Colour data used by <see cref="ColourEvaluator"/> and <see cref="StaminaEvaluator"/>.
|
||||
/// This is populated via <see cref="TaikoColourDifficultyPreprocessor"/>.
|
||||
/// </summary>
|
||||
public readonly TaikoDifficultyHitObjectColour Colour;
|
||||
public readonly TaikoColourData ColourData;
|
||||
|
||||
/// <summary>
|
||||
/// The adjusted BPM of this hit object, based on its slider velocity and scroll speed.
|
||||
/// </summary>
|
||||
public double EffectiveBPM;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new difficulty hit object.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The gameplay <see cref="HitObject"/> associated with this difficulty object.</param>
|
||||
/// <param name="lastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="hitObject"/>.</param>
|
||||
/// <param name="lastLastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="lastObject"/>.</param>
|
||||
/// <param name="clockRate">The rate of the gameplay clock. Modified by speed-changing mods.</param>
|
||||
/// <param name="objects">The list of all <see cref="DifficultyHitObject"/>s in the current beatmap.</param>
|
||||
/// <param name="centreHitObjects">The list of centre (don) <see cref="DifficultyHitObject"/>s in the current beatmap.</param>
|
||||
/// <param name="rimHitObjects">The list of rim (kat) <see cref="DifficultyHitObject"/>s in the current beatmap.</param>
|
||||
/// <param name="noteObjects">The list of <see cref="DifficultyHitObject"/>s that is a hit (i.e. not a drumroll or swell) in the current beatmap.</param>
|
||||
/// <param name="index">The position of this <see cref="DifficultyHitObject"/> in the <paramref name="objects"/> list.</param>
|
||||
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate,
|
||||
/// <param name="controlPointInfo">The control point info of the beatmap.</param>
|
||||
/// <param name="globalSliderVelocity">The global slider velocity of the beatmap.</param>
|
||||
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate,
|
||||
List<DifficultyHitObject> objects,
|
||||
List<TaikoDifficultyHitObject> centreHitObjects,
|
||||
List<TaikoDifficultyHitObject> rimHitObjects,
|
||||
List<TaikoDifficultyHitObject> noteObjects, int index)
|
||||
List<TaikoDifficultyHitObject> noteObjects, int index,
|
||||
ControlPointInfo controlPointInfo,
|
||||
double globalSliderVelocity)
|
||||
: base(hitObject, lastObject, clockRate, objects, index)
|
||||
{
|
||||
noteDifficultyHitObjects = noteObjects;
|
||||
|
||||
// Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor
|
||||
Colour = new TaikoDifficultyHitObjectColour();
|
||||
Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate);
|
||||
ColourData = new TaikoColourData();
|
||||
RhythmData = new TaikoRhythmData(this);
|
||||
|
||||
switch ((hitObject as Hit)?.Type)
|
||||
if (hitObject is Hit hit)
|
||||
{
|
||||
case HitType.Centre:
|
||||
MonoIndex = centreHitObjects.Count;
|
||||
centreHitObjects.Add(this);
|
||||
monoDifficultyHitObjects = centreHitObjects;
|
||||
break;
|
||||
switch (hit.Type)
|
||||
{
|
||||
case HitType.Centre:
|
||||
MonoIndex = centreHitObjects.Count;
|
||||
centreHitObjects.Add(this);
|
||||
monoDifficultyHitObjects = centreHitObjects;
|
||||
break;
|
||||
|
||||
case HitType.Rim:
|
||||
MonoIndex = rimHitObjects.Count;
|
||||
rimHitObjects.Add(this);
|
||||
monoDifficultyHitObjects = rimHitObjects;
|
||||
break;
|
||||
}
|
||||
case HitType.Rim:
|
||||
MonoIndex = rimHitObjects.Count;
|
||||
rimHitObjects.Add(this);
|
||||
monoDifficultyHitObjects = rimHitObjects;
|
||||
break;
|
||||
}
|
||||
|
||||
if (hitObject is Hit)
|
||||
{
|
||||
NoteIndex = noteObjects.Count;
|
||||
noteObjects.Add(this);
|
||||
}
|
||||
|
||||
// Using `hitObject.StartTime` causes floating point error differences
|
||||
double normalisedStartTime = StartTime * clockRate;
|
||||
|
||||
// Retrieve the timing point at the note's start time
|
||||
TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(normalisedStartTime);
|
||||
|
||||
// Calculate the slider velocity at the note's start time.
|
||||
double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalisedStartTime, clockRate);
|
||||
|
||||
EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List of most common rhythm changes in taiko maps.
|
||||
/// Calculates the slider velocity based on control point info and clock rate.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The general guidelines for the values are:
|
||||
/// <list type="bullet">
|
||||
/// <item>rhythm changes with ratio closer to 1 (that are <i>not</i> 1) are harder to play,</item>
|
||||
/// <item>speeding up is <i>generally</i> harder than slowing down (with exceptions of rhythm changes requiring a hand switch).</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms =
|
||||
private static double calculateSliderVelocity(ControlPointInfo controlPointInfo, double globalSliderVelocity, double startTime, double clockRate)
|
||||
{
|
||||
new TaikoDifficultyHitObjectRhythm(1, 1, 0.0),
|
||||
new TaikoDifficultyHitObjectRhythm(2, 1, 0.3),
|
||||
new TaikoDifficultyHitObjectRhythm(1, 2, 0.5),
|
||||
new TaikoDifficultyHitObjectRhythm(3, 1, 0.3),
|
||||
new TaikoDifficultyHitObjectRhythm(1, 3, 0.35),
|
||||
new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), // purposefully higher (requires hand switch in full alternating gameplay style)
|
||||
new TaikoDifficultyHitObjectRhythm(2, 3, 0.4),
|
||||
new TaikoDifficultyHitObjectRhythm(5, 4, 0.5),
|
||||
new TaikoDifficultyHitObjectRhythm(4, 5, 0.7)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns the closest rhythm change from <see cref="common_rhythms"/> required to hit this object.
|
||||
/// </summary>
|
||||
/// <param name="lastObject">The gameplay <see cref="HitObject"/> preceding this one.</param>
|
||||
/// <param name="lastLastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="lastObject"/>.</param>
|
||||
/// <param name="clockRate">The rate of the gameplay clock.</param>
|
||||
private TaikoDifficultyHitObjectRhythm getClosestRhythm(HitObject lastObject, HitObject lastLastObject, double clockRate)
|
||||
{
|
||||
double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate;
|
||||
double ratio = DeltaTime / prevLength;
|
||||
|
||||
return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First();
|
||||
var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime);
|
||||
return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate;
|
||||
}
|
||||
|
||||
public TaikoDifficultyHitObject? PreviousMono(int backwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex - (backwardsIndex + 1));
|
||||
@@ -139,5 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||
public TaikoDifficultyHitObject? PreviousNote(int backwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex - (backwardsIndex + 1));
|
||||
|
||||
public TaikoDifficultyHitObject? NextNote(int forwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex + (forwardsIndex + 1));
|
||||
|
||||
public double Interval => DeltaTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates the reading coefficient of taiko difficulty.
|
||||
/// </summary>
|
||||
public class Reading : StrainDecaySkill
|
||||
{
|
||||
protected override double SkillMultiplier => 1.0;
|
||||
protected override double StrainDecayBase => 0.4;
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
public Reading(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
// Drum Rolls and Swells are exempt.
|
||||
if (current.BaseObject is not Hit)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
var taikoObject = (TaikoDifficultyHitObject)current;
|
||||
int index = taikoObject.ColourData.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0;
|
||||
|
||||
currentStrain *= DifficultyCalculationUtils.Logistic(index, 4, -1 / 25.0, 0.5) + 0.5;
|
||||
|
||||
currentStrain *= StrainDecayBase;
|
||||
currentStrain += ReadingEvaluator.EvaluateDifficultyOf(taikoObject) * SkillMultiplier;
|
||||
|
||||
return currentStrain;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Utils;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
{
|
||||
@@ -16,158 +14,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
/// </summary>
|
||||
public class Rhythm : StrainDecaySkill
|
||||
{
|
||||
protected override double SkillMultiplier => 10;
|
||||
protected override double StrainDecayBase => 0;
|
||||
protected override double SkillMultiplier => 1.0;
|
||||
protected override double StrainDecayBase => 0.4;
|
||||
|
||||
/// <summary>
|
||||
/// The note-based decay for rhythm strain.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="StrainDecayBase"/> is not used here, as it's time- and not note-based.
|
||||
/// </remarks>
|
||||
private const double strain_decay = 0.96;
|
||||
private readonly double greatHitWindow;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of entries in <see cref="rhythmHistory"/>.
|
||||
/// </summary>
|
||||
private const int rhythm_history_max_length = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the last <see cref="rhythm_history_max_length"/> changes in note sequence rhythms.
|
||||
/// </summary>
|
||||
private readonly LimitedCapacityQueue<TaikoDifficultyHitObject> rhythmHistory = new LimitedCapacityQueue<TaikoDifficultyHitObject>(rhythm_history_max_length);
|
||||
|
||||
/// <summary>
|
||||
/// Contains the rolling rhythm strain.
|
||||
/// Used to apply per-note decay.
|
||||
/// </summary>
|
||||
private double currentStrain;
|
||||
|
||||
/// <summary>
|
||||
/// Number of notes since the last rhythm change has taken place.
|
||||
/// </summary>
|
||||
private int notesSinceRhythmChange;
|
||||
|
||||
public Rhythm(Mod[] mods)
|
||||
public Rhythm(Mod[] mods, double greatHitWindow)
|
||||
: base(mods)
|
||||
{
|
||||
this.greatHitWindow = greatHitWindow;
|
||||
}
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
// drum rolls and swells are exempt.
|
||||
if (!(current.BaseObject is Hit))
|
||||
{
|
||||
resetRhythmAndStrain();
|
||||
return 0.0;
|
||||
}
|
||||
double difficulty = RhythmEvaluator.EvaluateDifficultyOf(current, greatHitWindow);
|
||||
|
||||
currentStrain *= strain_decay;
|
||||
// To prevent abuse of exceedingly long intervals between awkward rhythms, we penalise its difficulty.
|
||||
double staminaDifficulty = StaminaEvaluator.EvaluateDifficultyOf(current) - 0.5; // Remove base strain
|
||||
difficulty *= DifficultyCalculationUtils.Logistic(staminaDifficulty, 1 / 15.0, 50.0);
|
||||
|
||||
TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
|
||||
notesSinceRhythmChange += 1;
|
||||
|
||||
// rhythm difficulty zero (due to rhythm not changing) => no rhythm strain.
|
||||
if (hitObject.Rhythm.Difficulty == 0.0)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double objectStrain = hitObject.Rhythm.Difficulty;
|
||||
|
||||
objectStrain *= repetitionPenalties(hitObject);
|
||||
objectStrain *= patternLengthPenalty(notesSinceRhythmChange);
|
||||
objectStrain *= speedPenalty(hitObject.DeltaTime);
|
||||
|
||||
// careful - needs to be done here since calls above read this value
|
||||
notesSinceRhythmChange = 0;
|
||||
|
||||
currentStrain += objectStrain;
|
||||
return currentStrain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a penalty to apply to the current hit object caused by repeating rhythm changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Repetitions of more recent patterns are associated with a higher penalty.
|
||||
/// </remarks>
|
||||
/// <param name="hitObject">The current hit object being considered.</param>
|
||||
private double repetitionPenalties(TaikoDifficultyHitObject hitObject)
|
||||
{
|
||||
double penalty = 1;
|
||||
|
||||
rhythmHistory.Enqueue(hitObject);
|
||||
|
||||
for (int mostRecentPatternsToCompare = 2; mostRecentPatternsToCompare <= rhythm_history_max_length / 2; mostRecentPatternsToCompare++)
|
||||
{
|
||||
for (int start = rhythmHistory.Count - mostRecentPatternsToCompare - 1; start >= 0; start--)
|
||||
{
|
||||
if (!samePattern(start, mostRecentPatternsToCompare))
|
||||
continue;
|
||||
|
||||
int notesSince = hitObject.Index - rhythmHistory[start].Index;
|
||||
penalty *= repetitionPenalty(notesSince);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return penalty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the rhythm change pattern starting at <paramref name="start"/> is a repeat of any of the
|
||||
/// <paramref name="mostRecentPatternsToCompare"/>.
|
||||
/// </summary>
|
||||
private bool samePattern(int start, int mostRecentPatternsToCompare)
|
||||
{
|
||||
for (int i = 0; i < mostRecentPatternsToCompare; i++)
|
||||
{
|
||||
if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - mostRecentPatternsToCompare + i].Rhythm)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a single rhythm repetition penalty.
|
||||
/// </summary>
|
||||
/// <param name="notesSince">Number of notes since the last repetition of a rhythm change.</param>
|
||||
private static double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince);
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a penalty based on the number of notes since the last rhythm change.
|
||||
/// Both rare and frequent rhythm changes are penalised.
|
||||
/// </summary>
|
||||
/// <param name="patternLength">Number of notes since the last rhythm change.</param>
|
||||
private static double patternLengthPenalty(int patternLength)
|
||||
{
|
||||
double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0);
|
||||
double longPatternPenalty = Math.Clamp(2.5 - 0.15 * patternLength, 0.0, 1.0);
|
||||
return Math.Min(shortPatternPenalty, longPatternPenalty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a penalty for objects that do not require alternating hands.
|
||||
/// </summary>
|
||||
/// <param name="deltaTime">Time (in milliseconds) since the last hit object.</param>
|
||||
private double speedPenalty(double deltaTime)
|
||||
{
|
||||
if (deltaTime < 80) return 1;
|
||||
if (deltaTime < 210) return Math.Max(0, 1.4 - 0.005 * deltaTime);
|
||||
|
||||
resetRhythmAndStrain();
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the rolling strain value and <see cref="notesSinceRhythmChange"/> counter.
|
||||
/// </summary>
|
||||
private void resetRhythmAndStrain()
|
||||
{
|
||||
currentStrain = 0.0;
|
||||
notesSinceRhythmChange = 0;
|
||||
return difficulty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
@@ -18,7 +19,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
private double skillMultiplier => 1.1;
|
||||
private double strainDecayBase => 0.4;
|
||||
|
||||
private readonly bool singleColourStamina;
|
||||
public readonly bool SingleColourStamina;
|
||||
private readonly bool isConvert;
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
@@ -27,10 +29,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
/// </summary>
|
||||
/// <param name="mods">Mods for use in skill calculations.</param>
|
||||
/// <param name="singleColourStamina">Reads when Stamina is from a single coloured pattern.</param>
|
||||
public Stamina(Mod[] mods, bool singleColourStamina)
|
||||
/// <param name="isConvert">Determines if the currently evaluated beatmap is converted.</param>
|
||||
public Stamina(Mod[] mods, bool singleColourStamina, bool isConvert)
|
||||
: base(mods)
|
||||
{
|
||||
this.singleColourStamina = singleColourStamina;
|
||||
SingleColourStamina = singleColourStamina;
|
||||
this.isConvert = isConvert;
|
||||
}
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
@@ -42,14 +46,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
|
||||
// Safely prevents previous strains from shifting as new notes are added.
|
||||
var currentObject = current as TaikoDifficultyHitObject;
|
||||
int index = currentObject?.Colour.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0;
|
||||
int index = currentObject?.ColourData.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0;
|
||||
|
||||
if (singleColourStamina)
|
||||
return currentStrain / (1 + Math.Exp(-(index - 10) / 2.0));
|
||||
double monolengthBonus = isConvert ? 1 : 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30);
|
||||
|
||||
return currentStrain;
|
||||
if (SingleColourStamina)
|
||||
return DifficultyCalculationUtils.Logistic(-(index - 10) / 2.0, currentStrain);
|
||||
|
||||
return currentStrain * monolengthBonus;
|
||||
}
|
||||
|
||||
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => singleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime);
|
||||
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => SingleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
public class TaikoDifficultyAttributes : DifficultyAttributes
|
||||
{
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the rhythm skill.
|
||||
/// </summary>
|
||||
public double RhythmDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the reading skill.
|
||||
/// </summary>
|
||||
public double ReadingDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the colour skill.
|
||||
/// </summary>
|
||||
public double ColourDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the stamina skill.
|
||||
/// </summary>
|
||||
[JsonProperty("stamina_difficulty")]
|
||||
public double StaminaDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -22,41 +36,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
[JsonProperty("mono_stamina_factor")]
|
||||
public double MonoStaminaFactor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the rhythm skill.
|
||||
/// </summary>
|
||||
[JsonProperty("rhythm_difficulty")]
|
||||
public double RhythmDifficulty { get; set; }
|
||||
public double RhythmTopStrains { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the colour skill.
|
||||
/// </summary>
|
||||
[JsonProperty("colour_difficulty")]
|
||||
public double ColourDifficulty { get; set; }
|
||||
public double ColourTopStrains { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the hardest parts of the map.
|
||||
/// </summary>
|
||||
[JsonProperty("peak_difficulty")]
|
||||
public double PeakDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("great_hit_window")]
|
||||
public double GreatHitWindow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("ok_hit_window")]
|
||||
public double OkHitWindow { get; set; }
|
||||
public double StaminaTopStrains { get; set; }
|
||||
|
||||
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
||||
{
|
||||
@@ -64,8 +48,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
yield return v;
|
||||
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
|
||||
yield return (ATTRIB_ID_MONO_STAMINA_FACTOR, MonoStaminaFactor);
|
||||
}
|
||||
|
||||
@@ -74,8 +56,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||
OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW];
|
||||
MonoStaminaFactor = values[ATTRIB_ID_MONO_STAMINA_FACTOR];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Scoring;
|
||||
@@ -21,11 +23,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
public class TaikoDifficultyCalculator : DifficultyCalculator
|
||||
{
|
||||
private const double difficulty_multiplier = 0.084375;
|
||||
private const double rhythm_skill_multiplier = 0.2 * difficulty_multiplier;
|
||||
private const double rhythm_skill_multiplier = 0.65 * difficulty_multiplier;
|
||||
private const double reading_skill_multiplier = 0.100 * difficulty_multiplier;
|
||||
private const double colour_skill_multiplier = 0.375 * difficulty_multiplier;
|
||||
private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier;
|
||||
private const double stamina_skill_multiplier = 0.445 * difficulty_multiplier;
|
||||
|
||||
public override int Version => 20241007;
|
||||
private double strainLengthBonus;
|
||||
private double patternMultiplier;
|
||||
|
||||
private bool isConvert;
|
||||
|
||||
public override int Version => 20250306;
|
||||
|
||||
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
@@ -34,12 +42,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
{
|
||||
HitWindows hitWindows = new TaikoHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
isConvert = beatmap.BeatmapInfo.Ruleset.OnlineID == 0;
|
||||
|
||||
return new Skill[]
|
||||
{
|
||||
new Rhythm(mods),
|
||||
new Rhythm(mods, hitWindows.WindowFor(HitResult.Great) / clockRate),
|
||||
new Reading(mods),
|
||||
new Colour(mods),
|
||||
new Stamina(mods, false),
|
||||
new Stamina(mods, true)
|
||||
new Stamina(mods, false, isConvert),
|
||||
new Stamina(mods, true, isConvert)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,21 +67,30 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||
{
|
||||
List<DifficultyHitObject> difficultyHitObjects = new List<DifficultyHitObject>();
|
||||
List<TaikoDifficultyHitObject> centreObjects = new List<TaikoDifficultyHitObject>();
|
||||
List<TaikoDifficultyHitObject> rimObjects = new List<TaikoDifficultyHitObject>();
|
||||
List<TaikoDifficultyHitObject> noteObjects = new List<TaikoDifficultyHitObject>();
|
||||
var difficultyHitObjects = new List<DifficultyHitObject>();
|
||||
var centreObjects = new List<TaikoDifficultyHitObject>();
|
||||
var rimObjects = new List<TaikoDifficultyHitObject>();
|
||||
var noteObjects = new List<TaikoDifficultyHitObject>();
|
||||
|
||||
// Generate TaikoDifficultyHitObjects from the beatmap's hit objects.
|
||||
for (int i = 2; i < beatmap.HitObjects.Count; i++)
|
||||
{
|
||||
difficultyHitObjects.Add(
|
||||
new TaikoDifficultyHitObject(
|
||||
beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects,
|
||||
centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count)
|
||||
);
|
||||
difficultyHitObjects.Add(new TaikoDifficultyHitObject(
|
||||
beatmap.HitObjects[i],
|
||||
beatmap.HitObjects[i - 1],
|
||||
clockRate,
|
||||
difficultyHitObjects,
|
||||
centreObjects,
|
||||
rimObjects,
|
||||
noteObjects,
|
||||
difficultyHitObjects.Count,
|
||||
beatmap.ControlPointInfo,
|
||||
beatmap.Difficulty.SliderMultiplier
|
||||
));
|
||||
}
|
||||
|
||||
TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects);
|
||||
TaikoRhythmDifficultyPreprocessor.ProcessAndAssign(noteObjects);
|
||||
|
||||
return difficultyHitObjects;
|
||||
}
|
||||
@@ -77,60 +100,53 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
return new TaikoDifficultyAttributes { Mods = mods };
|
||||
|
||||
Colour colour = (Colour)skills.First(x => x is Colour);
|
||||
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
|
||||
Stamina stamina = (Stamina)skills.First(x => x is Stamina);
|
||||
Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina);
|
||||
bool isRelax = mods.Any(h => h is TaikoModRelax);
|
||||
|
||||
var rhythm = skills.OfType<Rhythm>().Single();
|
||||
var reading = skills.OfType<Reading>().Single();
|
||||
var colour = skills.OfType<Colour>().Single();
|
||||
var stamina = skills.OfType<Stamina>().Single(s => !s.SingleColourStamina);
|
||||
var singleColourStamina = skills.OfType<Stamina>().Single(s => s.SingleColourStamina);
|
||||
|
||||
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
|
||||
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
|
||||
double readingRating = reading.DifficultyValue() * reading_skill_multiplier;
|
||||
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
|
||||
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier;
|
||||
double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier;
|
||||
double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5);
|
||||
|
||||
double combinedRating = combinedDifficultyValue(rhythm, colour, stamina);
|
||||
double colourDifficultStrains = colour.CountTopWeightedStrains();
|
||||
double rhythmDifficultStrains = rhythm.CountTopWeightedStrains();
|
||||
double staminaDifficultStrains = stamina.CountTopWeightedStrains();
|
||||
|
||||
// As we don't have pattern integration in osu!taiko, we apply the other two skills relative to rhythm.
|
||||
patternMultiplier = Math.Pow(staminaRating * colourRating, 0.10);
|
||||
|
||||
strainLengthBonus = 1
|
||||
+ Math.Min(Math.Max((staminaDifficultStrains - 1000) / 3700, 0), 0.15)
|
||||
+ Math.Min(Math.Max((staminaRating - 7.0) / 1.0, 0), 0.05);
|
||||
|
||||
double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax, isConvert);
|
||||
double starRating = rescale(combinedRating * 1.4);
|
||||
|
||||
// TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system.
|
||||
if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0)
|
||||
{
|
||||
starRating *= 0.925;
|
||||
// For maps with low colour variance and high stamina requirement, multiple inputs are more likely to be abused.
|
||||
if (colourRating < 2 && staminaRating > 8)
|
||||
starRating *= 0.80;
|
||||
}
|
||||
|
||||
HitWindows hitWindows = new TaikoHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes
|
||||
{
|
||||
StarRating = starRating,
|
||||
Mods = mods,
|
||||
RhythmDifficulty = rhythmRating,
|
||||
ReadingDifficulty = readingRating,
|
||||
ColourDifficulty = colourRating,
|
||||
StaminaDifficulty = staminaRating,
|
||||
MonoStaminaFactor = monoStaminaFactor,
|
||||
RhythmDifficulty = rhythmRating,
|
||||
ColourDifficulty = colourRating,
|
||||
PeakDifficulty = combinedRating,
|
||||
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
|
||||
OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate,
|
||||
RhythmTopStrains = rhythmDifficultStrains,
|
||||
ColourTopStrains = colourDifficultStrains,
|
||||
StaminaTopStrains = staminaDifficultStrains,
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
};
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a final re-scaling of the star rating.
|
||||
/// </summary>
|
||||
/// <param name="sr">The raw star rating value before re-scaling.</param>
|
||||
private double rescale(double sr)
|
||||
{
|
||||
if (sr < 0) return sr;
|
||||
|
||||
return 10.43 * Math.Log(sr / 8 + 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map.
|
||||
/// </summary>
|
||||
@@ -138,22 +154,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
/// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
|
||||
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
|
||||
/// </remarks>
|
||||
private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina)
|
||||
private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, bool isRelax, bool isConvert)
|
||||
{
|
||||
List<double> peaks = new List<double>();
|
||||
|
||||
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
|
||||
var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
|
||||
var readingPeaks = reading.GetCurrentStrainPeaks().ToList();
|
||||
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
|
||||
var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
|
||||
|
||||
for (int i = 0; i < colourPeaks.Count; i++)
|
||||
{
|
||||
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
|
||||
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
|
||||
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier;
|
||||
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier * patternMultiplier;
|
||||
double readingPeak = readingPeaks[i] * reading_skill_multiplier;
|
||||
double colourPeak = isRelax ? 0 : colourPeaks[i] * colour_skill_multiplier; // There is no colour difficulty in relax.
|
||||
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier * strainLengthBonus;
|
||||
staminaPeak /= isConvert || isRelax ? 1.5 : 1.0; // Available finger count is increased by 150%, thus we adjust accordingly.
|
||||
|
||||
double peak = norm(1.5, colourPeak, staminaPeak);
|
||||
peak = norm(2, peak, rhythmPeak);
|
||||
double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak, readingPeak);
|
||||
|
||||
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
||||
// These sections will not contribute to the difficulty.
|
||||
@@ -174,10 +192,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector.
|
||||
/// Applies a final re-scaling of the star rating.
|
||||
/// </summary>
|
||||
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param>
|
||||
/// <param name="values">The coefficients of the vector.</param>
|
||||
private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
|
||||
/// <param name="sr">The raw star rating value before re-scaling.</param>
|
||||
private static double rescale(double sr)
|
||||
{
|
||||
if (sr < 0)
|
||||
return sr;
|
||||
|
||||
return 10.43 * Math.Log(sr / 8 + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
@@ -21,6 +24,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
private int countMiss;
|
||||
private double? estimatedUnstableRate;
|
||||
|
||||
private double clockRate;
|
||||
private double greatHitWindow;
|
||||
|
||||
private double effectiveMissCount;
|
||||
|
||||
public TaikoPerformanceCalculator()
|
||||
@@ -36,7 +42,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
estimatedUnstableRate = computeDeviationUpperBound(taikoAttributes) * 10;
|
||||
|
||||
var track = new TrackVirtual(10000);
|
||||
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
clockRate = track.Rate;
|
||||
|
||||
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
|
||||
|
||||
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
||||
|
||||
HitWindows hitWindows = new TaikoHitWindows();
|
||||
hitWindows.SetDifficulty(difficulty.OverallDifficulty);
|
||||
|
||||
greatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
|
||||
estimatedUnstableRate = computeDeviationUpperBound() * 10;
|
||||
|
||||
// The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000.
|
||||
if (totalSuccessfulHits > 0)
|
||||
@@ -73,7 +93,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
|
||||
{
|
||||
double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0, 2.25) / 1150.0;
|
||||
double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating / 0.110) - 4.0;
|
||||
double difficultyValue = Math.Min(Math.Pow(baseDifficulty, 3) / 69052.51, Math.Pow(baseDifficulty, 2.25) / 1250.0);
|
||||
|
||||
difficultyValue *= 1 + 0.10 * Math.Max(0, attributes.StarRating - 10);
|
||||
|
||||
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
|
||||
difficultyValue *= lengthBonus;
|
||||
@@ -86,9 +109,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
if (score.Mods.Any(m => m is ModHidden))
|
||||
difficultyValue *= 1.025;
|
||||
|
||||
if (score.Mods.Any(m => m is ModHardRock))
|
||||
difficultyValue *= 1.10;
|
||||
|
||||
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
|
||||
difficultyValue *= Math.Max(1, 1.050 - Math.Min(attributes.MonoStaminaFactor / 50, 1) * lengthBonus);
|
||||
|
||||
@@ -97,14 +117,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
// Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps.
|
||||
double accScalingExponent = 2 + attributes.MonoStaminaFactor;
|
||||
double accScalingShift = 300 - 100 * attributes.MonoStaminaFactor;
|
||||
double accScalingShift = 500 - 100 * (attributes.MonoStaminaFactor * 3);
|
||||
|
||||
return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent);
|
||||
return difficultyValue * Math.Pow(DifficultyCalculationUtils.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent);
|
||||
}
|
||||
|
||||
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
|
||||
{
|
||||
if (attributes.GreatHitWindow <= 0 || estimatedUnstableRate == null)
|
||||
if (greatHitWindow <= 0 || estimatedUnstableRate == null)
|
||||
return 0;
|
||||
|
||||
double accuracyValue = Math.Pow(70 / estimatedUnstableRate.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0;
|
||||
@@ -123,58 +143,23 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
/// and the hit judgements, assuming the player's mean hit error is 0. The estimation is consistent in that
|
||||
/// two SS scores on the same map with the same settings will always return the same deviation.
|
||||
/// </summary>
|
||||
private double? computeDeviationUpperBound(TaikoDifficultyAttributes attributes)
|
||||
private double? computeDeviationUpperBound()
|
||||
{
|
||||
if (totalSuccessfulHits == 0 || attributes.GreatHitWindow <= 0)
|
||||
if (countGreat == 0 || greatHitWindow <= 0)
|
||||
return null;
|
||||
|
||||
double h300 = attributes.GreatHitWindow;
|
||||
double h100 = attributes.OkHitWindow;
|
||||
|
||||
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
|
||||
|
||||
// The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window.
|
||||
double? calcDeviationGreatWindow()
|
||||
{
|
||||
if (countGreat == 0) return null;
|
||||
double n = totalHits;
|
||||
|
||||
double n = totalHits;
|
||||
// Proportion of greats hit.
|
||||
double p = countGreat / n;
|
||||
|
||||
// Proportion of greats hit.
|
||||
double p = countGreat / n;
|
||||
// We can be 99% confident that p is at least this value.
|
||||
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||
|
||||
// We can be 99% confident that p is at least this value.
|
||||
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||
|
||||
// We can be 99% confident that the deviation is not higher than:
|
||||
return h300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
|
||||
}
|
||||
|
||||
// The upper bound on deviation, calculated with the ratio of 300s + 100s to objects, and the good hit window.
|
||||
// This will return a lower value than the first method when the number of 100s is high, but the miss count is low.
|
||||
double? calcDeviationGoodWindow()
|
||||
{
|
||||
if (totalSuccessfulHits == 0) return null;
|
||||
|
||||
double n = totalHits;
|
||||
|
||||
// Proportion of greats + goods hit.
|
||||
double p = totalSuccessfulHits / n;
|
||||
|
||||
// We can be 99% confident that p is at least this value.
|
||||
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||
|
||||
// We can be 99% confident that the deviation is not higher than:
|
||||
return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
|
||||
}
|
||||
|
||||
double? deviationGreatWindow = calcDeviationGreatWindow();
|
||||
double? deviationGoodWindow = calcDeviationGoodWindow();
|
||||
|
||||
if (deviationGreatWindow is null)
|
||||
return deviationGoodWindow;
|
||||
|
||||
return Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value);
|
||||
// We can be 99% confident that the deviation is not higher than:
|
||||
return greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound));
|
||||
}
|
||||
|
||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// The interface for objects that provide an interval value.
|
||||
/// </summary>
|
||||
public interface IHasInterval
|
||||
{
|
||||
/// <summary>
|
||||
/// The interval – ie delta time – between this object and a known previous object.
|
||||
/// </summary>
|
||||
double Interval { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Utils
|
||||
{
|
||||
public static class IntervalGroupingUtils
|
||||
{
|
||||
public static List<List<T>> GroupByInterval<T>(IReadOnlyList<T> objects) where T : IHasInterval
|
||||
{
|
||||
var groups = new List<List<T>>();
|
||||
|
||||
int i = 0;
|
||||
while (i < objects.Count)
|
||||
groups.Add(createNextGroup(objects, ref i));
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
private static List<T> createNextGroup<T>(IReadOnlyList<T> objects, ref int i) where T : IHasInterval
|
||||
{
|
||||
const double margin_of_error = 5;
|
||||
|
||||
// This never compares the first two elements in the group.
|
||||
// This sounds wrong but is apparently "as intended" (https://github.com/ppy/osu/pull/31636#discussion_r1942673329)
|
||||
var groupedObjects = new List<T> { objects[i] };
|
||||
i++;
|
||||
|
||||
for (; i < objects.Count - 1; i++)
|
||||
{
|
||||
if (!Precision.AlmostEquals(objects[i].Interval, objects[i + 1].Interval, margin_of_error))
|
||||
{
|
||||
// When an interval change occurs, include the object with the differing interval in the case it increased
|
||||
// See https://github.com/ppy/osu/pull/31636#discussion_r1942368372 for rationale.
|
||||
if (objects[i + 1].Interval > objects[i].Interval + margin_of_error)
|
||||
{
|
||||
groupedObjects.Add(objects[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
return groupedObjects;
|
||||
}
|
||||
|
||||
// No interval change occurred
|
||||
groupedObjects.Add(objects[i]);
|
||||
}
|
||||
|
||||
// Check if the last two objects in the object form a "flat" rhythm pattern within the specified margin of error.
|
||||
// If true, add the current object to the group and increment the index to process the next object.
|
||||
if (objects.Count > 2 && i < objects.Count && Precision.AlmostEquals(objects[^1].Interval, objects[^2].Interval, margin_of_error))
|
||||
{
|
||||
groupedObjects.Add(objects[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
return groupedObjects;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||
@@ -21,9 +19,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
public readonly IBindable<bool> LockPlayfieldAspectRange = new BindableBool(true);
|
||||
|
||||
[Resolved]
|
||||
private OsuGame? osuGame { get; set; }
|
||||
|
||||
public TaikoPlayfieldAdjustmentContainer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
@@ -60,19 +55,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
// Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions.
|
||||
relativeHeight = Math.Min(relativeHeight, 1f / 3f);
|
||||
|
||||
Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f));
|
||||
|
||||
// on mobile platforms where the base aspect ratio is wider, the taiko playfield
|
||||
// needs to be scaled down to remain playable.
|
||||
if (RuntimeInfo.IsMobile && osuGame != null)
|
||||
{
|
||||
const float base_aspect_ratio = 1024f / 768f;
|
||||
float gameAspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y;
|
||||
// this magic scale is unexplainable, but required so the playfield doesn't become too zoomed out as the aspect ratio increases.
|
||||
const float magic_scale = 1.25f;
|
||||
Scale *= magic_scale * new Vector2(base_aspect_ratio / gameAspectRatio);
|
||||
}
|
||||
|
||||
Scale = new Vector2(Parent!.ChildSize.Y / 768f * (relativeHeight / base_relative_height));
|
||||
Width = 1 / Scale.X;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps
|
||||
{
|
||||
public class BeatmapExtensionsTest
|
||||
{
|
||||
[Test]
|
||||
public void TestLengthCalculations()
|
||||
{
|
||||
var beatmap = new Beatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 5_000 },
|
||||
new HitCircle { StartTime = 300_000 },
|
||||
new Spinner { StartTime = 280_000, Duration = 40_000 }
|
||||
},
|
||||
Breaks =
|
||||
{
|
||||
new BreakPeriod(50_000, 75_000),
|
||||
new BreakPeriod(100_000, 150_000),
|
||||
}
|
||||
};
|
||||
|
||||
Assert.That(beatmap.CalculatePlayableBounds(), Is.EqualTo((5_000, 320_000)));
|
||||
Assert.That(beatmap.CalculatePlayableLength(), Is.EqualTo(315_000)); // 320_000 - 5_000
|
||||
Assert.That(beatmap.CalculateDrainLength(), Is.EqualTo(240_000)); // 315_000 - (25_000 + 50_000) = 315_000 - 75_000
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDrainLengthCannotGoNegative()
|
||||
{
|
||||
var beatmap = new Beatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 5_000 },
|
||||
new HitCircle { StartTime = 300_000 },
|
||||
new Spinner { StartTime = 280_000, Duration = 40_000 }
|
||||
},
|
||||
Breaks =
|
||||
{
|
||||
new BreakPeriod(0, 350_000),
|
||||
}
|
||||
};
|
||||
|
||||
Assert.That(beatmap.CalculatePlayableBounds(), Is.EqualTo((5_000, 320_000)));
|
||||
Assert.That(beatmap.CalculatePlayableLength(), Is.EqualTo(315_000)); // 320_000 - 5_000
|
||||
Assert.That(beatmap.CalculateDrainLength(), Is.EqualTo(0)); // break period encompasses entire beatmap
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,6 +404,35 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestComboColourCountIsLimitedToEight()
|
||||
{
|
||||
var decoder = new LegacySkinDecoder();
|
||||
|
||||
using (var resStream = TestResources.OpenResource("too-many-combo-colours.osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var comboColors = decoder.Decode(stream).ComboColours;
|
||||
|
||||
Debug.Assert(comboColors != null);
|
||||
|
||||
Color4[] expectedColors =
|
||||
{
|
||||
new Color4(142, 199, 255, 255),
|
||||
new Color4(255, 128, 128, 255),
|
||||
new Color4(128, 255, 255, 255),
|
||||
new Color4(128, 255, 128, 255),
|
||||
new Color4(255, 187, 255, 255),
|
||||
new Color4(255, 177, 140, 255),
|
||||
new Color4(100, 100, 100, 255),
|
||||
new Color4(142, 199, 255, 255),
|
||||
};
|
||||
Assert.AreEqual(expectedColors.Length, comboColors.Count);
|
||||
for (int i = 0; i < expectedColors.Length; i++)
|
||||
Assert.AreEqual(expectedColors[i], comboColors[i]);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGetLastObjectTime()
|
||||
{
|
||||
|
||||
@@ -28,6 +28,7 @@ using osu.Game.Skinning;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps.Formats
|
||||
{
|
||||
@@ -184,6 +185,32 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
Assert.That(decodedSlider.Path.ControlPoints.Count, Is.EqualTo(5));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlyEightComboColoursEncoded()
|
||||
{
|
||||
var beatmapSkin = new LegacyBeatmapSkin(new BeatmapInfo(), null)
|
||||
{
|
||||
Configuration =
|
||||
{
|
||||
CustomComboColours =
|
||||
{
|
||||
new Color4(1, 1, 1, 255),
|
||||
new Color4(2, 2, 2, 255),
|
||||
new Color4(3, 3, 3, 255),
|
||||
new Color4(4, 4, 4, 255),
|
||||
new Color4(5, 5, 5, 255),
|
||||
new Color4(6, 6, 6, 255),
|
||||
new Color4(7, 7, 7, 255),
|
||||
new Color4(8, 8, 8, 255),
|
||||
new Color4(9, 9, 9, 255),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy((new Beatmap(), beatmapSkin)), string.Empty);
|
||||
Assert.That(decodedAfterEncode.skin.Configuration.CustomComboColours, Has.Count.EqualTo(8));
|
||||
}
|
||||
|
||||
private bool areComboColoursEqual(IHasComboColours a, IHasComboColours b)
|
||||
{
|
||||
// equal to null, no need to SequenceEqual
|
||||
@@ -212,6 +239,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
{
|
||||
var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader);
|
||||
var beatmapSkin = new TestLegacySkin(beatmaps_resource_store, name);
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
beatmapSkin.Configuration = new LegacySkinDecoder().Decode(reader);
|
||||
return (convert(beatmap), beatmapSkin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ using Humanizer;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.NonVisual.Multiplayer
|
||||
@@ -16,6 +16,13 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
|
||||
[HeadlessTest]
|
||||
public partial class StatefulMultiplayerClientTest : MultiplayerTestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
|
||||
WaitForJoined();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserAddedOnJoin()
|
||||
{
|
||||
@@ -72,10 +79,6 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
|
||||
|
||||
AddStep("create room initially in gameplay", () =>
|
||||
{
|
||||
var newRoom = new Room();
|
||||
newRoom.CopyFrom(SelectedRoom.Value!);
|
||||
|
||||
newRoom.RoomID = null;
|
||||
MultiplayerClient.RoomSetupAction = room =>
|
||||
{
|
||||
room.State = MultiplayerRoomState.Playing;
|
||||
@@ -86,13 +89,32 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
|
||||
});
|
||||
};
|
||||
|
||||
RoomManager.CreateRoom(newRoom);
|
||||
MultiplayerClient.JoinRoom(MultiplayerClient.ServerSideRooms.Single()).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for room join", () => RoomJoined);
|
||||
checkPlayingUserCount(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestJoinRoomWithManyUsers()
|
||||
{
|
||||
AddStep("leave room", () => MultiplayerClient.LeaveRoom());
|
||||
AddUntilStep("wait for room part", () => !RoomJoined);
|
||||
|
||||
AddStep("create room with many users", () =>
|
||||
{
|
||||
MultiplayerClient.RoomSetupAction = room =>
|
||||
{
|
||||
room.Users.AddRange(Enumerable.Range(PLAYER_1_ID, 100).Select(id => new MultiplayerRoomUser(id)));
|
||||
};
|
||||
|
||||
MultiplayerClient.JoinRoom(MultiplayerClient.ServerSideRooms.Single()).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for room join", () => RoomJoined);
|
||||
}
|
||||
|
||||
private void checkPlayingUserCount(int expectedCount)
|
||||
=> AddAssert($"{"user".ToQuantity(expectedCount)} playing", () => MultiplayerClient.CurrentMatchPlayingUserIds.Count == expectedCount);
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
|
||||
namespace osu.Game.Tests.OnlinePlay
|
||||
{
|
||||
[HeadlessTest]
|
||||
public partial class TestSceneOnlinePlaySubScreenStack : OnlinePlayTestScene
|
||||
{
|
||||
private ScreenStack stack = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
Child = stack = new OnlinePlaySubScreenStack
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
};
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestBindablesDisabledWhenRequested()
|
||||
{
|
||||
AddAssert("bindables not disabled", () => Beatmap.Disabled || Ruleset.Disabled || SelectedMods.Disabled, () => Is.False);
|
||||
|
||||
AddStep("push screen that disables bindables", () => stack.Push(new ScreenWithExternalBindableDisablement(true)));
|
||||
AddAssert("bindables disabled", () => Beatmap.Disabled && Ruleset.Disabled && SelectedMods.Disabled, () => Is.True);
|
||||
|
||||
AddStep("push screen that does not disable bindables", () => stack.Push(new ScreenWithExternalBindableDisablement(false)));
|
||||
AddAssert("bindables not disabled", () => Beatmap.Disabled || Ruleset.Disabled || SelectedMods.Disabled, () => Is.False);
|
||||
|
||||
AddStep("exit one screen", () => stack.Exit());
|
||||
AddAssert("bindables disabled", () => Beatmap.Disabled && Ruleset.Disabled && SelectedMods.Disabled, () => Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModsResetWhenExitToLounge()
|
||||
{
|
||||
AddStep("push lounge", () => stack.Push(new PlaylistsLoungeSubScreen()));
|
||||
|
||||
AddStep("push screen with mod", () => stack.Push(new ScreenWithMod(new OsuModDoubleTime())));
|
||||
AddUntilStep("wait for screen to load", () => ((OsuScreen)stack.CurrentScreen).IsLoaded);
|
||||
AddAssert("mod set", () => SelectedMods.Value.Count, () => Is.GreaterThan(0));
|
||||
|
||||
AddStep("exit to lounge", () => stack.Exit());
|
||||
AddAssert("mods reset", () => SelectedMods.Value.Count, () => Is.Zero);
|
||||
}
|
||||
|
||||
private partial class ScreenWithExternalBindableDisablement : OsuScreen
|
||||
{
|
||||
public override bool DisallowExternalBeatmapRulesetChanges { get; }
|
||||
|
||||
public ScreenWithExternalBindableDisablement(bool disableBindables)
|
||||
{
|
||||
DisallowExternalBeatmapRulesetChanges = disableBindables;
|
||||
}
|
||||
}
|
||||
|
||||
private partial class ScreenWithMod : OsuScreen
|
||||
{
|
||||
private readonly Mod mod;
|
||||
|
||||
public ScreenWithMod(Mod mod)
|
||||
{
|
||||
this.mod = mod;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
Mods.Value = [mod];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
AudioFilename: 03. Renatus - Soleily 192kbps.mp3
|
||||
AudioLeadIn: 0
|
||||
PreviewTime: 164471
|
||||
Countdown: 0
|
||||
SampleSet: Soft
|
||||
StackLeniency: 0.7
|
||||
Mode: 0
|
||||
LetterboxInBreaks: 0
|
||||
WidescreenStoryboard: 0
|
||||
|
||||
[Editor]
|
||||
Bookmarks: 11505,22054,32604,43153,53703,64252,74802,85351,95901,106450,116999,119637,130186,140735,151285,161834,164471,175020,185570,196119,206669,209306
|
||||
DistanceSpacing: 1.8
|
||||
BeatDivisor: 4
|
||||
GridSize: 4
|
||||
TimelineZoom: 2
|
||||
|
||||
[Metadata]
|
||||
Title:Renatus
|
||||
TitleUnicode:Renatus
|
||||
Artist:Soleily
|
||||
ArtistUnicode:Soleily
|
||||
Creator:Gamu
|
||||
Version:Insane
|
||||
Source:
|
||||
Tags:MBC7 Unisphere 地球ヤバイEP Chikyu Yabai
|
||||
BeatmapID:557821
|
||||
BeatmapSetID:241526
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:6.5
|
||||
CircleSize:4
|
||||
OverallDifficulty:8
|
||||
ApproachRate:9
|
||||
SliderMultiplier:1.8
|
||||
SliderTickRate:2
|
||||
|
||||
[Events]
|
||||
//Background and Video events
|
||||
0,0,"machinetop_background.jpg",0,0
|
||||
//Break Periods
|
||||
2,122474,140135
|
||||
//Storyboard Layer 0 (Background)
|
||||
//Storyboard Layer 1 (Fail)
|
||||
//Storyboard Layer 2 (Pass)
|
||||
//Storyboard Layer 3 (Foreground)
|
||||
//Storyboard Sound Samples
|
||||
|
||||
[TimingPoints]
|
||||
956,329.67032967033,4,2,0,60,1,0
|
||||
|
||||
|
||||
[Colours]
|
||||
Combo1:142,199,255
|
||||
Combo2:255,128,128
|
||||
Combo3:128,255,255
|
||||
Combo4:128,255,128
|
||||
Combo5:255,187,255
|
||||
Combo6:255,177,140
|
||||
Combo7:100,100,100
|
||||
Combo8:142,199,255
|
||||
Combo9:255,128,128
|
||||
Combo10:128,255,255
|
||||
Combo11:128,255,128
|
||||
Combo12:255,187,255
|
||||
Combo13:255,177,140
|
||||
Combo14:100,100,100
|
||||
|
||||
[HitObjects]
|
||||
192,168,956,6,0,P|184:128|200:80,1,90,4|0,1:2|0:0,0:0:0:0:
|
||||
@@ -6,6 +6,8 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Metadata;
|
||||
@@ -13,9 +15,11 @@ using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.SelectV2.Leaderboards;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.Metadata;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
{
|
||||
@@ -57,6 +61,39 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUseTheseModsUnavailableIfNoFreeMods()
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
RoomID = 1234,
|
||||
Name = "Daily Challenge: June 4, 2024",
|
||||
Playlist =
|
||||
[
|
||||
new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First())
|
||||
{
|
||||
RequiredMods = [new APIMod(new OsuModTraceable())],
|
||||
AllowedMods = []
|
||||
}
|
||||
],
|
||||
EndDate = DateTimeOffset.Now.AddHours(12),
|
||||
Category = RoomCategory.DailyChallenge
|
||||
};
|
||||
|
||||
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
|
||||
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
|
||||
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||
AddUntilStep("wait for pushed", () => screen.IsCurrentScreen());
|
||||
AddStep("force transforms to finish", () => FinishTransforms(true));
|
||||
AddStep("right click second score", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<LeaderboardScoreV2>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
AddAssert("use these mods not present",
|
||||
() => this.ChildrenOfType<OsuContextMenu>().All(m => m.Items.All(item => item.Text.Value != "Use these mods")));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNotifications()
|
||||
{
|
||||
|
||||
@@ -94,6 +94,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.AsNonNull().ID)?.Value.DeletePending == true);
|
||||
|
||||
AddUntilStep("wait for default beatmap", () => Editor.Beatmap.Value is DummyWorkingBeatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -171,6 +173,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return difficultyName != null && difficultyName != firstDifficultyName;
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddAssert("created difficulty has timing point", () =>
|
||||
{
|
||||
var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single();
|
||||
@@ -215,6 +219,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return difficultyName != null && difficultyName != previousDifficultyName;
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = previousDifficultyName = Guid.NewGuid().ToString());
|
||||
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
|
||||
AddStep("add effect points", () =>
|
||||
@@ -239,6 +245,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return difficultyName != null && difficultyName != previousDifficultyName;
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddAssert("created difficulty has timing point", () =>
|
||||
{
|
||||
var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single();
|
||||
@@ -287,6 +295,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return difficultyName != null && difficultyName != firstDifficultyName;
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddAssert("created difficulty has timing point", () =>
|
||||
{
|
||||
var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single();
|
||||
@@ -367,6 +377,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return difficultyName != null && difficultyName != originalDifficultyName;
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddAssert("created difficulty has copy suffix in name", () => EditorBeatmap.BeatmapInfo.DifficultyName == copyDifficultyName);
|
||||
AddAssert("created difficulty has timing point", () =>
|
||||
{
|
||||
@@ -377,7 +389,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4);
|
||||
AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2);
|
||||
|
||||
ensureEditorLoaded();
|
||||
AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified);
|
||||
|
||||
AddAssert("online ID not copied", () => EditorBeatmap.BeatmapInfo.OnlineID == -1);
|
||||
|
||||
AddStep("save beatmap", () => Editor.Save());
|
||||
@@ -440,6 +454,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return difficultyName != null && difficultyName != originalDifficultyName;
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddStep("save without changes", () => Editor.Save());
|
||||
|
||||
AddAssert("collection still points to old beatmap", () => !collection.BeatmapMD5Hashes.Contains(EditorBeatmap.BeatmapInfo.MD5Hash)
|
||||
@@ -477,6 +493,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
|
||||
return difficultyName != null && difficultyName != "New Difficulty";
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddAssert("new difficulty has correct name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "New Difficulty (1)");
|
||||
AddAssert("new difficulty persisted", () =>
|
||||
{
|
||||
@@ -514,6 +533,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return difficultyName != null && difficultyName != duplicate_difficulty_name;
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name);
|
||||
AddStep("try to save beatmap", () => Editor.Save());
|
||||
AddAssert("beatmap set not corrupted", () =>
|
||||
@@ -540,6 +561,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1);
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new CatchRuleset().RulesetInfo));
|
||||
|
||||
AddUntilStep("wait for created", () =>
|
||||
@@ -547,7 +570,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
|
||||
return difficultyName != null && difficultyName != duplicate_difficulty_name;
|
||||
});
|
||||
AddUntilStep("wait for editor load", () => Editor.IsLoaded && DialogOverlay.IsLoaded);
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[]
|
||||
{
|
||||
@@ -584,6 +608,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
|
||||
return difficultyName != null && difficultyName == "New Difficulty";
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddAssert("new difficulty persisted", () =>
|
||||
{
|
||||
var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId);
|
||||
@@ -610,6 +637,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
|
||||
return difficultyName != null && difficultyName == "New Difficulty (1)";
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddAssert("new difficulty persisted", () =>
|
||||
{
|
||||
var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId);
|
||||
@@ -735,6 +765,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("other audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3"));
|
||||
}
|
||||
|
||||
private void ensureEditorLoaded() => AddUntilStep("wait for editor load", () => Editor.IsLoaded && DialogOverlay.IsLoaded);
|
||||
|
||||
private void createNewDifficulty()
|
||||
{
|
||||
string? currentDifficulty = null;
|
||||
@@ -748,13 +780,14 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
|
||||
AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction());
|
||||
|
||||
AddUntilStep("wait for created", () =>
|
||||
{
|
||||
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
|
||||
return difficultyName != null && difficultyName != currentDifficulty;
|
||||
});
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddUntilStep("wait for editor load", () => Editor.IsLoaded);
|
||||
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
|
||||
AddUntilStep("wait for load", () => Editor.ChildrenOfType<SetupScreen>().Any());
|
||||
}
|
||||
@@ -765,7 +798,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep($"switch to difficulty #{index + 1}", () =>
|
||||
Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.ElementAt(index)));
|
||||
|
||||
AddUntilStep("wait for editor load", () => Editor.IsLoaded);
|
||||
ensureEditorLoaded();
|
||||
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
|
||||
AddUntilStep("wait for load", () => Editor.ChildrenOfType<SetupScreen>().Any());
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@@ -14,8 +15,10 @@ using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
@@ -58,19 +61,63 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContextMenu()
|
||||
public void TestRightClickDuringEmptyPlacementTogglesNewCombo()
|
||||
{
|
||||
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
|
||||
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
|
||||
|
||||
AddStep("move mouse away from placed circle", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single().ScreenSpaceDrawQuad.TopLeft + Vector2.One));
|
||||
|
||||
AddAssert("new combo false", () => this.ChildrenOfType<NewComboTernaryButton>().Single().Current.Value, () => Is.EqualTo(TernaryState.False));
|
||||
AddStep("click right mouse", () => InputManager.Click(MouseButton.Right));
|
||||
AddAssert("new combo true", () => this.ChildrenOfType<NewComboTernaryButton>().Single().Current.Value, () => Is.EqualTo(TernaryState.True));
|
||||
AddAssert("context menu not visible", () => !Editor.ChildrenOfType<OsuContextMenu>().Any(c => c.IsPresent));
|
||||
|
||||
AddStep("click right mouse", () => InputManager.Click(MouseButton.Right));
|
||||
AddAssert("new combo false", () => this.ChildrenOfType<NewComboTernaryButton>().Single().Current.Value, () => Is.EqualTo(TernaryState.False));
|
||||
AddAssert("context menu not visible", () => !Editor.ChildrenOfType<OsuContextMenu>().Any(c => c.IsPresent));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRightClickDuringPlacementDeletes()
|
||||
{
|
||||
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
|
||||
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
|
||||
|
||||
AddStep("click right mouse", () => InputManager.Click(MouseButton.Right));
|
||||
|
||||
AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Has.Exactly(0).Items);
|
||||
AddAssert("circle not selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Exactly(0).Items);
|
||||
AddAssert("context menu not visible", () => !Editor.ChildrenOfType<OsuContextMenu>().Any(c => c.IsPresent));
|
||||
AddAssert("new combo false", () => this.ChildrenOfType<NewComboTernaryButton>().Single().Current.Value, () => Is.EqualTo(TernaryState.False));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRightClickDuringSelectionShowsContextMenu()
|
||||
{
|
||||
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
|
||||
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
|
||||
AddStep("delete with right mouse", () =>
|
||||
{
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
AddAssert("circle not removed", () => EditorBeatmap.HitObjects, () => Has.One.Items);
|
||||
// ensure the circle we're selecting is not a new combo so we can assert
|
||||
// new combo doesn't happen to get toggled by right click.
|
||||
AddStep("seek forward", () => EditorClock.Seek(1000));
|
||||
AddStep("place second circle", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("two circles added", () => EditorBeatmap.HitObjects, () => Has.Exactly(2).Items);
|
||||
AddAssert("context menu not visible", () => !Editor.ChildrenOfType<OsuContextMenu>().Any(c => c.IsPresent));
|
||||
|
||||
AddStep("select selection tool", () => InputManager.Key(Key.Number1));
|
||||
AddStep("click right mouse", () => InputManager.Click(MouseButton.Right));
|
||||
|
||||
AddAssert("circle not removed", () => EditorBeatmap.HitObjects, () => Has.Exactly(2).Items);
|
||||
AddAssert("circle selected", () => EditorBeatmap.SelectedHitObjects, () => Has.One.Items);
|
||||
AddAssert("context menu visible", () => Editor.ChildrenOfType<OsuContextMenu>().Any(c => c.IsPresent));
|
||||
AddAssert("new combo false", () => this.ChildrenOfType<NewComboTernaryButton>().Single().Current.Value, () => Is.EqualTo(TernaryState.False));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -128,12 +128,12 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("Press alt down", () => InputManager.PressKey(Key.AltLeft));
|
||||
AddStep("Scroll by 3", () => InputManager.ScrollBy(new Vector2(0, 3)));
|
||||
AddAssert("Box not at 0", () => !Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft));
|
||||
AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X));
|
||||
AddAssert("Box 1/2 at 1/2", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.5f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.5f * scrollQuad.Size.X));
|
||||
|
||||
// Scroll out at 0.25
|
||||
AddStep("Scroll by -3", () => InputManager.ScrollBy(new Vector2(0, -3)));
|
||||
AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft));
|
||||
AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X));
|
||||
AddAssert("Box 1/2 at 1/2", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.5f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.5f * scrollQuad.Size.X));
|
||||
AddStep("Release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
||||
}
|
||||
|
||||
|
||||
@@ -40,11 +40,16 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
breakTracker = new TestBreakTracker(),
|
||||
breakOverlay = new BreakOverlay(true, new ScoreProcessor(new OsuRuleset()))
|
||||
breakOverlay = new BreakOverlay(new ScoreProcessor(new OsuRuleset()))
|
||||
{
|
||||
ProcessCustomClock = false,
|
||||
BreakTracker = breakTracker,
|
||||
}
|
||||
},
|
||||
new LetterboxOverlay
|
||||
{
|
||||
ProcessCustomClock = false,
|
||||
BreakTracker = breakTracker,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Screens.Play.Break;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneLetterboxOverlay : OsuTestScene
|
||||
{
|
||||
public TestSceneLetterboxOverlay()
|
||||
{
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
new LetterboxOverlay()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
|
||||
GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState);
|
||||
TestSpectatorClient spectatorClient = new TestSpectatorClient();
|
||||
TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestMultiplayerRoomManager(new TestRoomRequestsHandler()));
|
||||
TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestRoomRequestsHandler());
|
||||
|
||||
AddStep("create spectator list", () =>
|
||||
{
|
||||
@@ -75,8 +75,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddRepeatStep("remove random user", () => ((ISpectatorClient)spectatorClient).UserEndedWatching(
|
||||
spectatorClient.WatchingUsers[RNG.Next(spectatorClient.WatchingUsers.Count)].OnlineID), 5);
|
||||
|
||||
AddStep("change font to venera", () => list.Font.Value = Typeface.Venera);
|
||||
AddStep("change font to torus", () => list.Font.Value = Typeface.Torus);
|
||||
AddStep("change font to venera", () => list.HeaderFont.Value = Typeface.Venera);
|
||||
AddStep("change font to torus", () => list.HeaderFont.Value = Typeface.Torus);
|
||||
AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1));
|
||||
|
||||
AddStep("enter break", () => playingState.Value = LocalUserPlayingState.Break);
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
var mockLounge = new Mock<LoungeSubScreen>();
|
||||
var mockLounge = new Mock<IOnlinePlayLounge>();
|
||||
mockLounge
|
||||
.Setup(l => l.Join(It.IsAny<Room>(), It.IsAny<string>(), It.IsAny<Action<Room>>(), It.IsAny<Action<string>>()))
|
||||
.Callback<Room, string, Action<Room>, Action<string>>((_, _, _, d) =>
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public partial class TestSceneDrawableRoomParticipantsList : OnlinePlayTestScene
|
||||
{
|
||||
private Room room = null!;
|
||||
private DrawableRoomParticipantsList list = null!;
|
||||
|
||||
public override void SetUpSteps()
|
||||
@@ -23,7 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("create list", () =>
|
||||
{
|
||||
SelectedRoom.Value = new Room
|
||||
room = new Room
|
||||
{
|
||||
Name = "test room",
|
||||
Host = new APIUser
|
||||
@@ -33,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
};
|
||||
|
||||
Child = list = new DrawableRoomParticipantsList(SelectedRoom.Value)
|
||||
Child = list = new DrawableRoomParticipantsList(room)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@@ -119,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("4 circles displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == 4);
|
||||
AddAssert("46 hidden users", () => list.ChildrenOfType<DrawableRoomParticipantsList.HiddenUserCount>().Single().Count == 46);
|
||||
|
||||
AddStep("remove from end", () => removeUserAt(SelectedRoom.Value!.RecentParticipants.Count - 1));
|
||||
AddStep("remove from end", () => removeUserAt(room.RecentParticipants.Count - 1));
|
||||
AddAssert("4 circles displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == 4);
|
||||
AddAssert("45 hidden users", () => list.ChildrenOfType<DrawableRoomParticipantsList.HiddenUserCount>().Single().Count == 45);
|
||||
|
||||
@@ -138,18 +139,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
private void addUser(int id)
|
||||
{
|
||||
SelectedRoom.Value!.RecentParticipants = SelectedRoom.Value!.RecentParticipants.Append(new APIUser
|
||||
room.RecentParticipants = room.RecentParticipants.Append(new APIUser
|
||||
{
|
||||
Id = id,
|
||||
Username = $"User {id}"
|
||||
}).ToArray();
|
||||
SelectedRoom.Value!.ParticipantCount++;
|
||||
room.ParticipantCount++;
|
||||
}
|
||||
|
||||
private void removeUserAt(int index)
|
||||
{
|
||||
SelectedRoom.Value!.RecentParticipants = SelectedRoom.Value!.RecentParticipants.Where(u => !u.Equals(SelectedRoom.Value!.RecentParticipants[index])).ToArray();
|
||||
SelectedRoom.Value!.ParticipantCount--;
|
||||
room.RecentParticipants = room.RecentParticipants.Where(u => !u.Equals(room.RecentParticipants[index])).ToArray();
|
||||
room.ParticipantCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,15 +16,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public partial class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene
|
||||
{
|
||||
private Room room = null!;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("create area", () =>
|
||||
{
|
||||
SelectedRoom.Value = new Room();
|
||||
|
||||
Child = new MatchBeatmapDetailArea(SelectedRoom.Value)
|
||||
Child = new MatchBeatmapDetailArea(room = new Room())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@@ -36,9 +36,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
private void createNewItem()
|
||||
{
|
||||
SelectedRoom.Value!.Playlist = SelectedRoom.Value.Playlist.Append(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
room.Playlist = room.Playlist.Append(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
ID = SelectedRoom.Value.Playlist.Count,
|
||||
ID = room.Playlist.Count,
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||
RequiredMods = new[]
|
||||
{
|
||||
|
||||
@@ -61,9 +61,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("create leaderboard", () =>
|
||||
{
|
||||
SelectedRoom.Value = new Room { RoomID = 3 };
|
||||
|
||||
Child = new MatchLeaderboard(SelectedRoom.Value)
|
||||
Child = new MatchLeaderboard(new Room { RoomID = 3 })
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
|
||||
@@ -24,6 +24,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("reset", () =>
|
||||
{
|
||||
leaderboard?.RemoveAndDisposeImmediately();
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
|
||||
@@ -42,6 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
private BeatmapManager beatmapManager { get; set; } = null!;
|
||||
|
||||
private MultiSpectatorScreen spectatorScreen = null!;
|
||||
private Room room = null!;
|
||||
|
||||
private readonly List<MultiplayerRoomUser> playingUsers = new List<MultiplayerRoomUser>();
|
||||
|
||||
@@ -63,6 +65,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("clear playing users", () => playingUsers.Clear());
|
||||
|
||||
AddStep("create room", () => room = CreateDefaultRoom());
|
||||
AddStep("join room", () => JoinRoom(room));
|
||||
WaitForJoined();
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
@@ -456,7 +462,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
applyToBeatmap?.Invoke(Beatmap.Value);
|
||||
|
||||
LoadScreen(spectatorScreen = new MultiSpectatorScreen(SelectedRoom.Value!, playingUsers.ToArray()));
|
||||
LoadScreen(spectatorScreen = new MultiSpectatorScreen(room, playingUsers.ToArray()));
|
||||
});
|
||||
|
||||
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded));
|
||||
|
||||
@@ -33,7 +33,6 @@ using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Match;
|
||||
@@ -58,7 +57,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
private TestMultiplayerComponents multiplayerComponents = null!;
|
||||
|
||||
private TestMultiplayerClient multiplayerClient => multiplayerComponents.MultiplayerClient;
|
||||
private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
@@ -257,7 +255,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("create room", () =>
|
||||
{
|
||||
roomManager.AddServerSideRoom(new Room
|
||||
multiplayerClient.AddServerSideRoom(new Room
|
||||
{
|
||||
Name = "Test Room",
|
||||
Playlist =
|
||||
@@ -286,7 +284,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("create room", () =>
|
||||
{
|
||||
roomManager.AddServerSideRoom(new Room
|
||||
multiplayerClient.AddServerSideRoom(new Room
|
||||
{
|
||||
Name = "Test Room",
|
||||
Playlist =
|
||||
@@ -336,7 +334,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("create room", () =>
|
||||
{
|
||||
roomManager.AddServerSideRoom(new Room
|
||||
multiplayerClient.AddServerSideRoom(new Room
|
||||
{
|
||||
Name = "Test Room",
|
||||
Password = "password",
|
||||
@@ -789,7 +787,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("create room", () =>
|
||||
{
|
||||
roomManager.AddServerSideRoom(new Room
|
||||
multiplayerClient.AddServerSideRoom(new Room
|
||||
{
|
||||
Name = "Test Room",
|
||||
QueueMode = QueueMode.AllPlayers,
|
||||
@@ -807,11 +805,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("wait for room", () => this.ChildrenOfType<DrawableRoom>().Any());
|
||||
AddStep("select room", () => InputManager.Key(Key.Down));
|
||||
|
||||
AddStep("disable polling", () => this.ChildrenOfType<ListingPollingComponent>().Single().TimeBetweenPolls.Value = 0);
|
||||
AddStep("disable polling", () => this.ChildrenOfType<LoungeListingPoller>().Single().TimeBetweenPolls.Value = 0);
|
||||
AddStep("change server-side settings", () =>
|
||||
{
|
||||
roomManager.ServerSideRooms[0].Name = "New name";
|
||||
roomManager.ServerSideRooms[0].Playlist =
|
||||
multiplayerClient.ServerSideRooms[0].Name = "New name";
|
||||
multiplayerClient.ServerSideRooms[0].Playlist =
|
||||
[
|
||||
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
|
||||
{
|
||||
@@ -828,7 +826,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("local room has correct settings", () =>
|
||||
{
|
||||
var localRoom = this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().Room;
|
||||
return localRoom.Name == roomManager.ServerSideRooms[0].Name && localRoom.Playlist.Single().ID == 2;
|
||||
return localRoom.Name == multiplayerClient.ServerSideRooms[0].Name && localRoom.Playlist.Single().ID == 2;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,45 +16,32 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public partial class TestSceneMultiplayerLoungeSubScreen : OnlinePlayTestScene
|
||||
public partial class TestSceneMultiplayerLoungeSubScreen : MultiplayerTestScene
|
||||
{
|
||||
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
|
||||
|
||||
private LoungeSubScreen loungeScreen = null!;
|
||||
private Room? lastJoinedRoom;
|
||||
private string? lastJoinedPassword;
|
||||
private MultiplayerLoungeSubScreen loungeScreen = null!;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("push screen", () => LoadScreen(loungeScreen = new MultiplayerLoungeSubScreen()));
|
||||
|
||||
AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen());
|
||||
|
||||
AddStep("bind to event", () =>
|
||||
{
|
||||
lastJoinedRoom = null;
|
||||
lastJoinedPassword = null;
|
||||
RoomManager.JoinRoomRequested = onRoomJoined;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestJoinRoomWithoutPassword()
|
||||
{
|
||||
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false));
|
||||
createRooms(GenerateRooms(1, withPassword: false));
|
||||
AddStep("select room", () => InputManager.Key(Key.Down));
|
||||
AddStep("join room", () => InputManager.Key(Key.Enter));
|
||||
|
||||
AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First());
|
||||
AddAssert("room join password correct", () => lastJoinedPassword == null);
|
||||
AddAssert("room joined", () => MultiplayerClient.RoomJoined);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPopoverHidesOnBackButton()
|
||||
{
|
||||
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
|
||||
createRooms(GenerateRooms(1, withPassword: true));
|
||||
AddStep("select room", () => InputManager.Key(Key.Down));
|
||||
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
|
||||
|
||||
@@ -67,18 +54,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("hit escape", () => InputManager.Key(Key.Escape));
|
||||
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
|
||||
|
||||
AddAssert("room not joined", () => !MultiplayerClient.RoomJoined);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPopoverHidesOnLeavingScreen()
|
||||
{
|
||||
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
|
||||
createRooms(GenerateRooms(1, withPassword: true));
|
||||
AddStep("select room", () => InputManager.Key(Key.Down));
|
||||
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
|
||||
|
||||
AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
|
||||
AddStep("exit screen", () => Stack.Exit());
|
||||
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
|
||||
|
||||
AddAssert("room not joined", () => !MultiplayerClient.RoomJoined);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -86,16 +77,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
|
||||
|
||||
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
|
||||
createRooms(GenerateRooms(1, withPassword: true));
|
||||
AddStep("select room", () => InputManager.Key(Key.Down));
|
||||
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
|
||||
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
|
||||
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "wrong");
|
||||
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
|
||||
|
||||
AddAssert("room not joined", () => loungeScreen.IsCurrentScreen());
|
||||
AddAssert("still at lounge", () => loungeScreen.IsCurrentScreen());
|
||||
AddUntilStep("password prompt still visible", () => passwordEntryPopover!.State.Value == Visibility.Visible);
|
||||
AddAssert("textbox still focused", () => InputManager.FocusedDrawable is OsuPasswordTextBox);
|
||||
|
||||
AddAssert("room not joined", () => !MultiplayerClient.RoomJoined);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -103,16 +96,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
|
||||
|
||||
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
|
||||
createRooms(GenerateRooms(1, withPassword: true));
|
||||
AddStep("select room", () => InputManager.Key(Key.Down));
|
||||
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
|
||||
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
|
||||
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "wrong");
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
|
||||
AddAssert("room not joined", () => loungeScreen.IsCurrentScreen());
|
||||
AddAssert("still at lounge", () => loungeScreen.IsCurrentScreen());
|
||||
AddUntilStep("password prompt still visible", () => passwordEntryPopover!.State.Value == Visibility.Visible);
|
||||
AddAssert("textbox still focused", () => InputManager.FocusedDrawable is OsuPasswordTextBox);
|
||||
|
||||
AddAssert("room not joined", () => !MultiplayerClient.RoomJoined);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -120,15 +115,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
|
||||
|
||||
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
|
||||
createRooms(GenerateRooms(1, withPassword: true));
|
||||
AddStep("select room", () => InputManager.Key(Key.Down));
|
||||
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
|
||||
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
|
||||
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
|
||||
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
|
||||
|
||||
AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First());
|
||||
AddAssert("room join password correct", () => lastJoinedPassword == "password");
|
||||
AddUntilStep("room joined", () => MultiplayerClient.RoomJoined);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -136,21 +130,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
|
||||
|
||||
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
|
||||
createRooms(GenerateRooms(1, withPassword: true));
|
||||
AddStep("select room", () => InputManager.Key(Key.Down));
|
||||
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
|
||||
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
|
||||
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
|
||||
AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First());
|
||||
AddAssert("room join password correct", () => lastJoinedPassword == "password");
|
||||
AddAssert("room joined", () => MultiplayerClient.RoomJoined);
|
||||
}
|
||||
|
||||
private void onRoomJoined(Room room, string? password)
|
||||
private void createRooms(params Room[] rooms)
|
||||
{
|
||||
lastJoinedRoom = room;
|
||||
lastJoinedPassword = password;
|
||||
AddStep("create rooms", () =>
|
||||
{
|
||||
foreach (var room in rooms)
|
||||
API.Queue(new CreateRoomRequest(room));
|
||||
});
|
||||
|
||||
AddStep("refresh lounge", () => loungeScreen.RefreshRooms());
|
||||
}
|
||||
|
||||
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
private TestMultiplayerMatchSongSelect songSelect = null!;
|
||||
private Live<BeatmapSetInfo> importedBeatmapSet = null!;
|
||||
private Room room = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager configManager { get; set; } = null!;
|
||||
@@ -58,6 +59,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Add(beatmapStore);
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("create room", () => room = CreateDefaultRoom());
|
||||
AddStep("join room", () => JoinRoom(room));
|
||||
WaitForJoined();
|
||||
}
|
||||
|
||||
private void setUp()
|
||||
{
|
||||
AddStep("create song select", () =>
|
||||
@@ -66,7 +76,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Beatmap.SetDefault();
|
||||
SelectedMods.SetDefault();
|
||||
|
||||
LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value!));
|
||||
LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(room));
|
||||
});
|
||||
|
||||
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded);
|
||||
@@ -138,8 +148,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("create song select", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist.Single().RulesetID = 2;
|
||||
songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value, SelectedRoom.Value.Playlist.Single());
|
||||
room.Playlist.Single().RulesetID = 2;
|
||||
songSelect = new TestMultiplayerMatchSongSelect(room, room.Playlist.Single());
|
||||
songSelect.OnLoadComplete += _ => Ruleset.Value = new TaikoRuleset().RulesetInfo;
|
||||
LoadScreen(songSelect);
|
||||
});
|
||||
|
||||
@@ -43,11 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
private MultiplayerMatchSubScreen screen = null!;
|
||||
private BeatmapManager beatmaps = null!;
|
||||
private BeatmapSetInfo importedSet = null!;
|
||||
|
||||
public TestSceneMultiplayerMatchSubScreen()
|
||||
: base(false)
|
||||
{
|
||||
}
|
||||
private Room room = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
@@ -66,8 +62,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("load match", () =>
|
||||
{
|
||||
SelectedRoom.Value = new Room { Name = "Test Room" };
|
||||
LoadScreen(screen = new TestMultiplayerMatchSubScreen(SelectedRoom.Value!));
|
||||
room = new Room { Name = "Test Room" };
|
||||
LoadScreen(screen = new TestMultiplayerMatchSubScreen(room));
|
||||
});
|
||||
|
||||
AddUntilStep("wait for load", () => screen.IsCurrentScreen());
|
||||
@@ -78,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add playlist item", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
@@ -97,7 +93,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add playlist item", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new TaikoRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
@@ -122,7 +118,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("set playlist", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
@@ -139,7 +135,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("set playlist", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo)
|
||||
{
|
||||
@@ -170,7 +166,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add playlist item with allowed mod", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
@@ -199,7 +195,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add playlist item with allowed mod", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
@@ -223,7 +219,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add playlist item with no allowed mods", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
@@ -246,7 +242,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add two playlist items", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo)
|
||||
{
|
||||
@@ -285,7 +281,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add playlist item", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
@@ -317,6 +313,29 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("score multiplier = 1.20", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeSettingsButtonVisibleForHost()
|
||||
{
|
||||
AddStep("add playlist item", () =>
|
||||
{
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
|
||||
}
|
||||
];
|
||||
});
|
||||
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
|
||||
|
||||
AddUntilStep("wait for join", () => RoomJoined);
|
||||
|
||||
AddUntilStep("button visible", () => this.ChildrenOfType<DrawableMatchRoom>().Single().ChangeSettingsButton?.Alpha, () => Is.GreaterThan(0));
|
||||
AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }));
|
||||
AddStep("make other user host", () => MultiplayerClient.TransferHost(PLAYER_1_ID));
|
||||
AddAssert("button hidden", () => this.ChildrenOfType<DrawableMatchRoom>().Single().ChangeSettingsButton?.Alpha, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen
|
||||
{
|
||||
[Resolved(canBeNull: true)]
|
||||
|
||||
@@ -28,9 +28,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public partial class TestSceneMultiplayerParticipantsList : MultiplayerTestScene
|
||||
{
|
||||
[SetUpSteps]
|
||||
public void SetupSteps()
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
|
||||
WaitForJoined();
|
||||
createNewParticipantsList();
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user