mirror of
https://github.com/ppy/osu.git
synced 2025-03-12 10:27:20 +08:00
Merge branch 'master' into map_info_on_mod_settings
This commit is contained in:
commit
589f56d20c
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.823.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.904.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
|
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
|
||||||
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
||||||
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
|
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
|
||||||
AddAssert("default slider velocity", () => lastObject.SliderVelocityBindable.IsDefault);
|
AddAssert("default slider velocity", () => lastObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
addPlacementSteps(times, positions);
|
addPlacementSteps(times, positions);
|
||||||
addPathCheckStep(times, positions);
|
addPathCheckStep(times, positions);
|
||||||
|
|
||||||
AddAssert("slider velocity changed", () => !lastObject.SliderVelocityBindable.IsDefault);
|
AddAssert("slider velocity changed", () => !lastObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -108,11 +108,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
double[] times = { 100, 300 };
|
double[] times = { 100, 300 };
|
||||||
float[] positions = { 200, 300 };
|
float[] positions = { 200, 300 };
|
||||||
addBlueprintStep(times, positions);
|
addBlueprintStep(times, positions);
|
||||||
AddAssert("default slider velocity", () => hitObject.SliderVelocityBindable.IsDefault);
|
AddAssert("default slider velocity", () => hitObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||||
|
|
||||||
addDragStartStep(times[1], positions[1]);
|
addDragStartStep(times[1], positions[1]);
|
||||||
AddMouseMoveStep(times[1], 400);
|
AddMouseMoveStep(times[1], 400);
|
||||||
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityBindable.IsDefault);
|
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
|||||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||||
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
|
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
|
||||||
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
||||||
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1
|
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
|
||||||
}.Yield();
|
}.Yield();
|
||||||
|
|
||||||
case IHasDuration endTime:
|
case IHasDuration endTime:
|
||||||
|
@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||||
{
|
{
|
||||||
// The SV setting may need to be changed for the current path.
|
// The SV setting may need to be changed for the current path.
|
||||||
var svBindable = hitObject.SliderVelocityBindable;
|
var svBindable = hitObject.SliderVelocityMultiplierBindable;
|
||||||
double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
|
double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
|
||||||
double requiredVelocity = path.ComputeRequiredVelocity();
|
double requiredVelocity = path.ComputeRequiredVelocity();
|
||||||
|
|
||||||
|
@ -28,17 +28,17 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
|
|
||||||
public int RepeatCount { get; set; }
|
public int RepeatCount { get; set; }
|
||||||
|
|
||||||
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
|
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
|
||||||
{
|
{
|
||||||
Precision = 0.01,
|
Precision = 0.01,
|
||||||
MinValue = 0.1,
|
MinValue = 0.1,
|
||||||
MaxValue = 10
|
MaxValue = 10
|
||||||
};
|
};
|
||||||
|
|
||||||
public double SliderVelocity
|
public double SliderVelocityMultiplier
|
||||||
{
|
{
|
||||||
get => SliderVelocityBindable.Value;
|
get => SliderVelocityMultiplierBindable.Value;
|
||||||
set => SliderVelocityBindable.Value = value;
|
set => SliderVelocityMultiplierBindable.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
@ -48,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
private double tickDistanceFactor;
|
private double tickDistanceFactor;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public double Velocity => velocityFactor * SliderVelocity;
|
public double Velocity => velocityFactor * SliderVelocityMultiplier;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public double TickDistance => tickDistanceFactor * SliderVelocity;
|
public double TickDistance => tickDistanceFactor * SliderVelocityMultiplier;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The length of one span of this <see cref="JuiceStream"/>.
|
/// The length of one span of this <see cref="JuiceStream"/>.
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Mods;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Mania.Replays;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Replays;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||||
|
{
|
||||||
|
public partial class TestSceneManiaModDoubleTime : ModTestScene
|
||||||
|
{
|
||||||
|
private const double offset = 18;
|
||||||
|
|
||||||
|
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHitWindowWithoutDoubleTime() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value != 1,
|
||||||
|
Autoplay = false,
|
||||||
|
Beatmap = new Beatmap
|
||||||
|
{
|
||||||
|
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
|
||||||
|
Difficulty = { OverallDifficulty = 10 },
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new Note { StartTime = 1000 }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReplayFrames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new ManiaReplayFrame(1000 + offset, ManiaAction.Key1)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHitWindowWithDoubleTime() => CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new ManiaModDoubleTime(),
|
||||||
|
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value == 1,
|
||||||
|
Autoplay = false,
|
||||||
|
Beatmap = new Beatmap
|
||||||
|
{
|
||||||
|
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
|
||||||
|
Difficulty = { OverallDifficulty = 10 },
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new Note { StartTime = 1000 }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReplayFrames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new ManiaReplayFrame(1000 + offset, ManiaAction.Key1)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
if (hitObject.LegacyBpmMultiplier.HasValue)
|
if (hitObject.LegacyBpmMultiplier.HasValue)
|
||||||
beatLength = timingPoint.BeatLength * hitObject.LegacyBpmMultiplier.Value;
|
beatLength = timingPoint.BeatLength * hitObject.LegacyBpmMultiplier.Value;
|
||||||
else if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
else if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
||||||
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
|
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocityMultiplier;
|
||||||
else
|
else
|
||||||
beatLength = timingPoint.BeatLength;
|
beatLength = timingPoint.BeatLength;
|
||||||
|
|
||||||
|
47
osu.Game.Rulesets.Mania/Mods/IManiaRateAdjustmentMod.cs
Normal file
47
osu.Game.Rulesets.Mania/Mods/IManiaRateAdjustmentMod.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Mania.Scoring;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Mods
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// May be attached to rate-adjustment mods to adjust hit windows adjust relative to gameplay rate.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Historically, in osu!mania, hit windows are expected to adjust relative to the gameplay rate such that the real-world hit window remains the same.
|
||||||
|
/// </remarks>
|
||||||
|
public interface IManiaRateAdjustmentMod : IApplicableToDifficulty, IApplicableToHitObject
|
||||||
|
{
|
||||||
|
BindableNumber<double> SpeedChange { get; }
|
||||||
|
|
||||||
|
HitWindows HitWindows { get; set; }
|
||||||
|
|
||||||
|
void IApplicableToDifficulty.ApplyToDifficulty(BeatmapDifficulty difficulty)
|
||||||
|
{
|
||||||
|
HitWindows = new ManiaHitWindows(SpeedChange.Value);
|
||||||
|
HitWindows.SetDifficulty(difficulty.OverallDifficulty);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
|
||||||
|
{
|
||||||
|
switch (hitObject)
|
||||||
|
{
|
||||||
|
case Note:
|
||||||
|
hitObject.HitWindows = HitWindows;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HoldNote hold:
|
||||||
|
hold.Head.HitWindows = HitWindows;
|
||||||
|
hold.Tail.HitWindows = HitWindows;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,14 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Rulesets.Mania.Scoring;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Mods
|
namespace osu.Game.Rulesets.Mania.Mods
|
||||||
{
|
{
|
||||||
public class ManiaModDaycore : ModDaycore
|
public class ManiaModDaycore : ModDaycore, IManiaRateAdjustmentMod
|
||||||
{
|
{
|
||||||
|
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Rulesets.Mania.Scoring;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Mods
|
namespace osu.Game.Rulesets.Mania.Mods
|
||||||
{
|
{
|
||||||
public class ManiaModDoubleTime : ModDoubleTime
|
public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod
|
||||||
{
|
{
|
||||||
|
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Rulesets.Mania.Scoring;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Mods
|
namespace osu.Game.Rulesets.Mania.Mods
|
||||||
{
|
{
|
||||||
public class ManiaModHalfTime : ModHalfTime
|
public class ManiaModHalfTime : ModHalfTime, IManiaRateAdjustmentMod
|
||||||
{
|
{
|
||||||
|
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,14 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Mania.Scoring;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Mods
|
namespace osu.Game.Rulesets.Mania.Mods
|
||||||
{
|
{
|
||||||
public class ManiaModNightcore : ModNightcore<ManiaHitObject>
|
public class ManiaModNightcore : ModNightcore<ManiaHitObject>, IManiaRateAdjustmentMod
|
||||||
{
|
{
|
||||||
|
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,25 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Scoring
|
namespace osu.Game.Rulesets.Mania.Scoring
|
||||||
{
|
{
|
||||||
public class ManiaHitWindows : HitWindows
|
public class ManiaHitWindows : HitWindows
|
||||||
{
|
{
|
||||||
|
private readonly double multiplier;
|
||||||
|
|
||||||
|
public ManiaHitWindows()
|
||||||
|
: this(1)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManiaHitWindows(double multiplier)
|
||||||
|
{
|
||||||
|
this.multiplier = multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
public override bool IsHitResultAllowed(HitResult result)
|
public override bool IsHitResultAllowed(HitResult result)
|
||||||
{
|
{
|
||||||
switch (result)
|
switch (result)
|
||||||
@ -22,5 +35,12 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override DifficultyRange[] GetRanges() => base.GetRanges().Select(r =>
|
||||||
|
new DifficultyRange(
|
||||||
|
r.Result,
|
||||||
|
r.Min * multiplier,
|
||||||
|
r.Average * multiplier,
|
||||||
|
r.Max * multiplier)).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
|||||||
Lookup = lookup;
|
Lookup = lookup;
|
||||||
ColumnIndex = columnIndex;
|
ColumnIndex = columnIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString() => $"[{nameof(ManiaSkinConfigurationLookup)} lookup:{Lookup} col:{ColumnIndex}]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
|||||||
new PathControlPoint(new Vector2(0, 6.25f))
|
new PathControlPoint(new Vector2(0, 6.25f))
|
||||||
}),
|
}),
|
||||||
RepeatCount = 1,
|
RepeatCount = 1,
|
||||||
SliderVelocity = 10
|
SliderVelocityMultiplier = 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
{
|
{
|
||||||
if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current) != false)
|
if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current, HitResult.Great) == ClickAction.Hit)
|
||||||
{
|
{
|
||||||
// force success
|
// force success
|
||||||
ApplyResult(r => r.Type = HitResult.Great);
|
ApplyResult(r => r.Type = HitResult.Great);
|
||||||
|
@ -13,6 +13,7 @@ using osu.Game.Rulesets.Objects.Types;
|
|||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private float? alphaAtMiss;
|
private float? alphaAtMiss;
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestHitCircleClassicMod()
|
public void TestHitCircleClassicModMiss()
|
||||||
{
|
{
|
||||||
AddStep("Create hit circle", () =>
|
AddStep("Create hit circle", () =>
|
||||||
{
|
{
|
||||||
@ -61,8 +62,27 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
|
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No early fade is expected to be applied if the hit circle has been hit.
|
||||||
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
public void TestHitCircleNoMod()
|
public void TestHitCircleClassicModHit()
|
||||||
|
{
|
||||||
|
TestDrawableHitCircle circle = null!;
|
||||||
|
|
||||||
|
AddStep("Create hit circle", () =>
|
||||||
|
{
|
||||||
|
SelectedMods.Value = new Mod[] { new OsuModClassic() };
|
||||||
|
circle = createCircle(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("Wait until circle is hit", () => circle.Result?.Type == HitResult.Great);
|
||||||
|
AddUntilStep("Wait for miss window", () => Clock.CurrentTime, () => Is.GreaterThanOrEqualTo(circle.HitObject.StartTime + circle.HitObject.HitWindows.WindowFor(HitResult.Miss)));
|
||||||
|
AddAssert("Check circle is still visible", () => circle.Alpha, () => Is.GreaterThan(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHitCircleNoModMiss()
|
||||||
{
|
{
|
||||||
AddStep("Create hit circle", () =>
|
AddStep("Create hit circle", () =>
|
||||||
{
|
{
|
||||||
@ -74,6 +94,16 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddAssert("Opaque when missed", () => alphaAtMiss == 1);
|
AddAssert("Opaque when missed", () => alphaAtMiss == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHitCircleNoModHit()
|
||||||
|
{
|
||||||
|
AddStep("Create hit circle", () =>
|
||||||
|
{
|
||||||
|
SelectedMods.Value = Array.Empty<Mod>();
|
||||||
|
createCircle(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSliderClassicMod()
|
public void TestSliderClassicMod()
|
||||||
{
|
{
|
||||||
@ -100,27 +130,33 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddAssert("Head circle opaque when missed", () => alphaAtMiss == 1);
|
AddAssert("Head circle opaque when missed", () => alphaAtMiss == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createCircle()
|
private TestDrawableHitCircle createCircle(bool shouldHit = false)
|
||||||
{
|
{
|
||||||
alphaAtMiss = null;
|
alphaAtMiss = null;
|
||||||
|
|
||||||
DrawableHitCircle drawableHitCircle = new DrawableHitCircle(new HitCircle
|
TestDrawableHitCircle drawableHitCircle = new TestDrawableHitCircle(new HitCircle
|
||||||
{
|
{
|
||||||
StartTime = Time.Current + 500,
|
StartTime = Time.Current + 500,
|
||||||
Position = new Vector2(250)
|
Position = new Vector2(250),
|
||||||
});
|
}, shouldHit);
|
||||||
|
|
||||||
|
drawableHitCircle.Scale = new Vector2(2f);
|
||||||
|
|
||||||
|
LoadComponent(drawableHitCircle);
|
||||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||||
mod.ApplyToDrawableHitObject(drawableHitCircle);
|
mod.ApplyToDrawableHitObject(drawableHitCircle);
|
||||||
|
|
||||||
drawableHitCircle.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
drawableHitCircle.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
|
|
||||||
drawableHitCircle.OnNewResult += (_, _) =>
|
drawableHitCircle.OnNewResult += (_, result) =>
|
||||||
{
|
{
|
||||||
alphaAtMiss = drawableHitCircle.Alpha;
|
if (!result.IsHit)
|
||||||
|
alphaAtMiss = drawableHitCircle.Alpha;
|
||||||
};
|
};
|
||||||
|
|
||||||
Child = drawableHitCircle;
|
Child = drawableHitCircle;
|
||||||
|
|
||||||
|
return drawableHitCircle;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createSlider()
|
private void createSlider()
|
||||||
@ -138,6 +174,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
drawableSlider.Scale = new Vector2(2f);
|
||||||
|
|
||||||
drawableSlider.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
drawableSlider.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
|
|
||||||
drawableSlider.OnLoadComplete += _ =>
|
drawableSlider.OnLoadComplete += _ =>
|
||||||
@ -145,12 +183,36 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||||
mod.ApplyToDrawableHitObject(drawableSlider.HeadCircle);
|
mod.ApplyToDrawableHitObject(drawableSlider.HeadCircle);
|
||||||
|
|
||||||
drawableSlider.HeadCircle.OnNewResult += (_, _) =>
|
drawableSlider.HeadCircle.OnNewResult += (_, result) =>
|
||||||
{
|
{
|
||||||
alphaAtMiss = drawableSlider.HeadCircle.Alpha;
|
if (!result.IsHit)
|
||||||
|
alphaAtMiss = drawableSlider.HeadCircle.Alpha;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
Child = drawableSlider;
|
Child = drawableSlider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected partial class TestDrawableHitCircle : DrawableHitCircle
|
||||||
|
{
|
||||||
|
private readonly bool shouldHit;
|
||||||
|
|
||||||
|
public TestDrawableHitCircle(HitCircle h, bool shouldHit)
|
||||||
|
: base(h)
|
||||||
|
{
|
||||||
|
this.shouldHit = shouldHit;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
|
{
|
||||||
|
if (shouldHit && !userTriggered && timeOffset >= 0)
|
||||||
|
{
|
||||||
|
// force success
|
||||||
|
ApplyResult(r => r.Type = HitResult.Great);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
base.CheckForResult(userTriggered, timeOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,17 +11,21 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Extensions.TypeExtensions;
|
using osu.Framework.Extensions.TypeExtensions;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Beatmaps.Formats;
|
using osu.Game.Beatmaps.Formats;
|
||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Replays;
|
using osu.Game.Rulesets.Osu.Replays;
|
||||||
using osu.Game.Rulesets.Osu.Scoring;
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
@ -32,7 +36,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
{
|
{
|
||||||
public partial class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene
|
public partial class TestSceneLegacyHitPolicy : RateAdjustedBeatmapTestScene
|
||||||
{
|
{
|
||||||
private readonly OsuHitWindows referenceHitWindows;
|
private readonly OsuHitWindows referenceHitWindows;
|
||||||
|
|
||||||
@ -43,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly string? exportLocation = null;
|
private readonly string? exportLocation = null;
|
||||||
|
|
||||||
public TestSceneObjectOrderedHitPolicy()
|
public TestSceneLegacyHitPolicy()
|
||||||
{
|
{
|
||||||
referenceHitWindows = new OsuHitWindows();
|
referenceHitWindows = new OsuHitWindows();
|
||||||
referenceHitWindows.SetDifficulty(0);
|
referenceHitWindows.SetDifficulty(0);
|
||||||
@ -83,6 +87,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
||||||
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
||||||
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
||||||
|
addClickActionAssert(0, ClickAction.Shake);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -119,6 +124,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
||||||
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
||||||
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
||||||
|
addClickActionAssert(0, ClickAction.Shake);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -155,6 +161,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
||||||
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
||||||
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
||||||
|
addClickActionAssert(0, ClickAction.Shake);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -191,7 +198,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
addJudgementAssert(hitObjects[0], HitResult.Meh);
|
addJudgementAssert(hitObjects[0], HitResult.Meh);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
||||||
addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
|
addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
|
||||||
addJudgementOffsetAssert(hitObjects[0], -90); // time_second_circle - first_circle_time - 90
|
addJudgementOffsetAssert(hitObjects[1], -190); // time_second_circle - first_circle_time - 90
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -229,13 +238,15 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
addJudgementAssert(hitObjects[1], HitResult.Ok);
|
addJudgementAssert(hitObjects[1], HitResult.Ok);
|
||||||
addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
|
addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
|
||||||
addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time
|
addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tests clicking a future circle after a slider's start time, but hitting all slider ticks.
|
/// Tests clicking a future circle after a slider's start time, but hitting the slider head and all slider ticks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
public void TestMissSliderHeadAndHitAllSliderTicks()
|
public void TestHitCircleBeforeSliderHead()
|
||||||
{
|
{
|
||||||
const double time_slider = 1500;
|
const double time_slider = 1500;
|
||||||
const double time_circle = 1510;
|
const double time_circle = 1510;
|
||||||
@ -267,10 +278,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } }
|
new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } }
|
||||||
});
|
});
|
||||||
|
|
||||||
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||||
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
||||||
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -314,6 +327,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||||
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
||||||
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -353,6 +368,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -391,6 +407,199 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||||
|
addClickActionAssert(0, ClickAction.Shake);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
|
addClickActionAssert(2, ClickAction.Hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOverlappingSliders()
|
||||||
|
{
|
||||||
|
const double time_first_slider = 1000;
|
||||||
|
const double time_second_slider = 1200;
|
||||||
|
Vector2 positionFirstSlider = new Vector2(100, 50);
|
||||||
|
Vector2 positionSecondSlider = new Vector2(100, 80);
|
||||||
|
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
||||||
|
|
||||||
|
var hitObjects = new List<OsuHitObject>
|
||||||
|
{
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
StartTime = time_first_slider,
|
||||||
|
Position = positionFirstSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
StartTime = time_second_slider,
|
||||||
|
Position = positionSecondSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton, OsuAction.RightButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint },
|
||||||
|
new OsuReplayFrame { Time = time_second_slider, Position = positionSecondSlider + new Vector2(0, 10), Actions = { OsuAction.LeftButton } },
|
||||||
|
});
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[0], HitResult.Ok);
|
||||||
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestStacksDoNotShake()
|
||||||
|
{
|
||||||
|
const double time_stack_start = 1000;
|
||||||
|
Vector2 position = new Vector2(80);
|
||||||
|
|
||||||
|
var hitObjects = Enumerable.Range(0, 20).Select(i => new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = time_stack_start + i * 100,
|
||||||
|
Position = position
|
||||||
|
}).Cast<OsuHitObject>().ToList();
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_stack_start - 450, Position = new Vector2(55), Actions = { OsuAction.LeftButton } },
|
||||||
|
});
|
||||||
|
|
||||||
|
addClickActionAssert(0, ClickAction.Ignore);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAutopilotReducesHittableRange()
|
||||||
|
{
|
||||||
|
const double time_circle = 1500;
|
||||||
|
Vector2 positionCircle = Vector2.Zero;
|
||||||
|
|
||||||
|
var hitObjects = new List<OsuHitObject>
|
||||||
|
{
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = time_circle,
|
||||||
|
Position = positionCircle
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_circle - 250, Position = positionCircle, Actions = { OsuAction.LeftButton } }
|
||||||
|
}, new Mod[] { new OsuModAutopilot() });
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
||||||
|
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
||||||
|
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
||||||
|
addClickActionAssert(0, ClickAction.Shake);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Ignore("Currently broken, first attempt at fixing broke even harder. See https://github.com/ppy/osu/issues/24743.")]
|
||||||
|
public void TestInputDoesNotFallThroughOverlappingSliders()
|
||||||
|
{
|
||||||
|
const double time_first_slider = 1000;
|
||||||
|
const double time_second_slider = 1250;
|
||||||
|
Vector2 positionFirstSlider = new Vector2(100, 50);
|
||||||
|
Vector2 positionSecondSlider = new Vector2(100, 80);
|
||||||
|
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
||||||
|
|
||||||
|
var hitObjects = new List<OsuHitObject>
|
||||||
|
{
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
StartTime = time_first_slider,
|
||||||
|
Position = positionFirstSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
StartTime = time_second_slider,
|
||||||
|
Position = positionSecondSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint },
|
||||||
|
});
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[0], HitResult.Ok);
|
||||||
|
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
|
||||||
|
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
||||||
|
// the slider head of the first slider prevents the second slider's head from being hit, so the judgement offset should be very late.
|
||||||
|
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, referenceHitWindows.WindowFor(HitResult.Meh));
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOverlappingObjectsDontBlockEachOtherWhenFullyFadedOut()
|
||||||
|
{
|
||||||
|
const double time_first_circle = 1000;
|
||||||
|
const double time_second_circle = 1200;
|
||||||
|
const double time_third_circle = 1400;
|
||||||
|
Vector2 positionFirstCircle = new Vector2(100);
|
||||||
|
Vector2 positionSecondCircle = new Vector2(200);
|
||||||
|
|
||||||
|
var hitObjects = new List<OsuHitObject>
|
||||||
|
{
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = time_first_circle,
|
||||||
|
Position = positionFirstCircle,
|
||||||
|
},
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = time_second_circle,
|
||||||
|
Position = positionSecondCircle,
|
||||||
|
},
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = time_third_circle,
|
||||||
|
Position = positionFirstCircle,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_first_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_circle + 50, Position = positionFirstCircle },
|
||||||
|
new OsuReplayFrame { Time = time_second_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } },
|
||||||
|
new OsuReplayFrame { Time = time_second_circle + 50, Position = positionSecondCircle },
|
||||||
|
new OsuReplayFrame { Time = time_third_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
|
||||||
|
new OsuReplayFrame { Time = time_third_circle + 50, Position = positionFirstCircle },
|
||||||
|
});
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||||
|
addJudgementOffsetAssert(hitObjects[0], 0);
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||||
|
addJudgementOffsetAssert(hitObjects[1], 0);
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[2], HitResult.Great);
|
||||||
|
addJudgementOffsetAssert(hitObjects[2], 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
||||||
@ -408,17 +617,36 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
|
private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
|
||||||
{
|
{
|
||||||
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
|
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
|
||||||
() => judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, () => Is.EqualTo(offset).Within(100));
|
() => judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addJudgementOffsetAssert(string name, Func<OsuHitObject?> hitObject, double offset)
|
||||||
|
{
|
||||||
|
AddAssert($"{name} @ judged at {offset}",
|
||||||
|
() => judgementResults.Single(r => r.HitObject == hitObject()).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addClickActionAssert(int inputIndex, ClickAction action)
|
||||||
|
=> AddAssert($"input #{inputIndex} resulted in {action}", () => testPolicy.ClickActions[inputIndex], () => Is.EqualTo(action));
|
||||||
|
|
||||||
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
||||||
private List<JudgementResult> judgementResults = null!;
|
private List<JudgementResult> judgementResults = null!;
|
||||||
|
private TestLegacyHitPolicy testPolicy = null!;
|
||||||
|
|
||||||
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames, [CallerMemberName] string testCaseName = "")
|
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames, IEnumerable<Mod>? extraMods = null, [CallerMemberName] string testCaseName = "")
|
||||||
{
|
{
|
||||||
|
List<Mod> mods = null!;
|
||||||
IBeatmap playableBeatmap = null!;
|
IBeatmap playableBeatmap = null!;
|
||||||
Score score = null!;
|
Score score = null!;
|
||||||
|
|
||||||
|
AddStep("set up mods", () =>
|
||||||
|
{
|
||||||
|
mods = new List<Mod> { new OsuModClassic() };
|
||||||
|
|
||||||
|
if (extraMods != null)
|
||||||
|
mods.AddRange(extraMods);
|
||||||
|
});
|
||||||
|
|
||||||
AddStep("create beatmap", () =>
|
AddStep("create beatmap", () =>
|
||||||
{
|
{
|
||||||
var cpi = new ControlPointInfo();
|
var cpi = new ControlPointInfo();
|
||||||
@ -461,7 +689,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
ScoreInfo =
|
ScoreInfo =
|
||||||
{
|
{
|
||||||
Ruleset = new OsuRuleset().RulesetInfo,
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
BeatmapInfo = playableBeatmap.BeatmapInfo
|
BeatmapInfo = playableBeatmap.BeatmapInfo,
|
||||||
|
Mods = mods.ToArray()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -495,7 +724,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
AddStep("load player", () =>
|
AddStep("load player", () =>
|
||||||
{
|
{
|
||||||
SelectedMods.Value = new[] { new OsuModClassic() };
|
SelectedMods.Value = mods.ToArray();
|
||||||
|
|
||||||
var p = new ScoreAccessibleReplayPlayer(score);
|
var p = new ScoreAccessibleReplayPlayer(score);
|
||||||
|
|
||||||
@ -513,6 +742,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||||
|
AddStep("Substitute hit policy", () =>
|
||||||
|
{
|
||||||
|
var playfield = currentPlayer.ChildrenOfType<OsuPlayfield>().Single();
|
||||||
|
var currentPolicy = playfield.HitPolicy;
|
||||||
|
playfield.HitPolicy = testPolicy = new TestLegacyHitPolicy(currentPolicy);
|
||||||
|
});
|
||||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -540,5 +775,24 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class TestLegacyHitPolicy : LegacyHitPolicy
|
||||||
|
{
|
||||||
|
private readonly IHitPolicy currentPolicy;
|
||||||
|
|
||||||
|
public TestLegacyHitPolicy(IHitPolicy currentPolicy)
|
||||||
|
{
|
||||||
|
this.currentPolicy = currentPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ClickAction> ClickActions { get; } = new List<ClickAction>();
|
||||||
|
|
||||||
|
public override ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result)
|
||||||
|
{
|
||||||
|
var action = currentPolicy.CheckHittable(hitObject, time, result);
|
||||||
|
ClickActions.Add(action);
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
StartTime = time_slider_start,
|
StartTime = time_slider_start,
|
||||||
Position = new Vector2(0, 0),
|
Position = new Vector2(0, 0),
|
||||||
SliderVelocity = velocity,
|
SliderVelocityMultiplier = velocity,
|
||||||
Path = new SliderPath(PathType.Linear, new[]
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
{
|
{
|
||||||
Vector2.Zero,
|
Vector2.Zero,
|
||||||
|
@ -349,7 +349,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
StartTime = time_slider_start,
|
StartTime = time_slider_start,
|
||||||
Position = new Vector2(0, 0),
|
Position = new Vector2(0, 0),
|
||||||
SliderVelocity = 0.1f,
|
SliderVelocityMultiplier = 0.1f,
|
||||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||||
{
|
{
|
||||||
Vector2.Zero,
|
Vector2.Zero,
|
||||||
|
@ -336,6 +336,52 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
addJudgementAssert(hitObjects[1], HitResult.IgnoreHit);
|
addJudgementAssert(hitObjects[1], HitResult.IgnoreHit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInputFallsThroughJudgedSliders()
|
||||||
|
{
|
||||||
|
const double time_first_slider = 1000;
|
||||||
|
const double time_second_slider = 1250;
|
||||||
|
Vector2 positionFirstSlider = new Vector2(100, 50);
|
||||||
|
Vector2 positionSecondSlider = new Vector2(100, 80);
|
||||||
|
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
||||||
|
|
||||||
|
var hitObjects = new List<OsuHitObject>
|
||||||
|
{
|
||||||
|
new TestSlider
|
||||||
|
{
|
||||||
|
StartTime = time_first_slider,
|
||||||
|
Position = positionFirstSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
new TestSlider
|
||||||
|
{
|
||||||
|
StartTime = time_second_slider,
|
||||||
|
Position = positionSecondSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint },
|
||||||
|
});
|
||||||
|
|
||||||
|
addJudgementAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, HitResult.Great);
|
||||||
|
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
|
||||||
|
addJudgementAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great);
|
||||||
|
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, -200);
|
||||||
|
}
|
||||||
|
|
||||||
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
||||||
{
|
{
|
||||||
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
|
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
|
||||||
@ -354,6 +400,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
() => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
|
() => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addJudgementOffsetAssert(string name, Func<OsuHitObject> hitObject, double offset)
|
||||||
|
{
|
||||||
|
AddAssert($"{name} @ judged at {offset}",
|
||||||
|
() => judgementResults.Single(r => r.HitObject == hitObject()).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
||||||
|
}
|
||||||
|
|
||||||
private ScoreAccessibleReplayPlayer currentPlayer;
|
private ScoreAccessibleReplayPlayer currentPlayer;
|
||||||
private List<JudgementResult> judgementResults;
|
private List<JudgementResult> judgementResults;
|
||||||
|
|
||||||
@ -399,7 +451,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
public TestSlider()
|
public TestSlider()
|
||||||
{
|
{
|
||||||
SliderVelocity = 0.1f;
|
SliderVelocityMultiplier = 0.1f;
|
||||||
|
|
||||||
DefaultsApplied += _ =>
|
DefaultsApplied += _ =>
|
||||||
{
|
{
|
||||||
|
@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
||||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
|
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
|
||||||
GenerateTicks = generateTicksData?.GenerateTicks ?? true,
|
GenerateTicks = generateTicksData?.GenerateTicks ?? true,
|
||||||
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1,
|
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1,
|
||||||
}.Yield();
|
}.Yield();
|
||||||
|
|
||||||
case IHasDuration endTimeData:
|
case IHasDuration endTimeData:
|
||||||
|
@ -85,9 +85,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
BeginPlacement();
|
BeginPlacement();
|
||||||
|
|
||||||
double? nearestSliderVelocity = (editorBeatmap.HitObjects
|
double? nearestSliderVelocity = (editorBeatmap.HitObjects
|
||||||
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocity;
|
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocityMultiplier;
|
||||||
|
|
||||||
HitObject.SliderVelocity = nearestSliderVelocity ?? 1;
|
HitObject.SliderVelocityMultiplier = nearestSliderVelocity ?? 1;
|
||||||
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
|
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
|
||||||
|
|
||||||
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.
|
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.
|
||||||
|
@ -11,6 +11,7 @@ using osu.Game.Rulesets.Objects;
|
|||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
@ -57,7 +58,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
var osuRuleset = (DrawableOsuRuleset)drawableRuleset;
|
var osuRuleset = (DrawableOsuRuleset)drawableRuleset;
|
||||||
|
|
||||||
if (ClassicNoteLock.Value)
|
if (ClassicNoteLock.Value)
|
||||||
osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy();
|
{
|
||||||
|
double hittableRange = OsuHitWindows.MISS_WINDOW - (drawableRuleset.Mods.OfType<OsuModAutopilot>().Any() ? 200 : 0);
|
||||||
|
osuRuleset.Playfield.HitPolicy = new LegacyHitPolicy(hittableRange);
|
||||||
|
}
|
||||||
|
|
||||||
usingHiddenFading = drawableRuleset.Mods.OfType<OsuModHidden>().SingleOrDefault()?.OnlyFadeApproachCircles.Value == false;
|
usingHiddenFading = drawableRuleset.Mods.OfType<OsuModHidden>().SingleOrDefault()?.OnlyFadeApproachCircles.Value == false;
|
||||||
}
|
}
|
||||||
@ -85,13 +89,16 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
private void applyEarlyFading(DrawableHitCircle circle)
|
private void applyEarlyFading(DrawableHitCircle circle)
|
||||||
{
|
{
|
||||||
circle.ApplyCustomUpdateState += (o, _) =>
|
circle.ApplyCustomUpdateState += (dho, state) =>
|
||||||
{
|
{
|
||||||
using (o.BeginAbsoluteSequence(o.StateUpdateTime))
|
using (dho.BeginAbsoluteSequence(dho.StateUpdateTime))
|
||||||
{
|
{
|
||||||
double okWindow = o.HitObject.HitWindows.WindowFor(HitResult.Ok);
|
if (state != ArmedState.Hit)
|
||||||
double lateMissFadeTime = o.HitObject.HitWindows.WindowFor(HitResult.Meh) - okWindow;
|
{
|
||||||
o.Delay(okWindow).FadeOut(lateMissFadeTime);
|
double okWindow = dho.HitObject.HitWindows.WindowFor(HitResult.Ok);
|
||||||
|
double lateMissFadeTime = dho.HitObject.HitWindows.WindowFor(HitResult.Meh) - okWindow;
|
||||||
|
dho.Delay(okWindow).FadeOut(lateMissFadeTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,19 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Mods
|
namespace osu.Game.Rulesets.Osu.Mods
|
||||||
{
|
{
|
||||||
public class OsuModDifficultyAdjust : ModDifficultyAdjust
|
public partial class OsuModDifficultyAdjust : ModDifficultyAdjust
|
||||||
{
|
{
|
||||||
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
||||||
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
|
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
|
||||||
@ -20,12 +26,13 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
ReadCurrentFromDifficulty = diff => diff.CircleSize,
|
ReadCurrentFromDifficulty = diff => diff.CircleSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(ApproachRateSettingsControl))]
|
||||||
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
|
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
|
||||||
{
|
{
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
|
ExtendedMinValue = -10,
|
||||||
ExtendedMaxValue = 11,
|
ExtendedMaxValue = 11,
|
||||||
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
|
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
|
||||||
};
|
};
|
||||||
@ -53,5 +60,34 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value;
|
if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value;
|
||||||
if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value;
|
if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private partial class ApproachRateSettingsControl : DifficultyAdjustSettingsControl
|
||||||
|
{
|
||||||
|
protected override RoundedSliderBar<float> CreateSlider(BindableNumber<float> current) =>
|
||||||
|
new ApproachRateSlider
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Current = current,
|
||||||
|
KeyboardStep = 0.1f,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A slider bar with more detailed approach rate info for its given value
|
||||||
|
/// </summary>
|
||||||
|
public partial class ApproachRateSlider : RoundedSliderBar<float>
|
||||||
|
{
|
||||||
|
public override LocalisableString TooltipText =>
|
||||||
|
(Current as BindableNumber<float>)?.MinValue < 0
|
||||||
|
? $"{base.TooltipText} ({getPreemptTime(Current.Value):0} ms)"
|
||||||
|
: base.TooltipText;
|
||||||
|
|
||||||
|
private double getPreemptTime(float approachRate)
|
||||||
|
{
|
||||||
|
var hitCircle = new HitCircle();
|
||||||
|
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { ApproachRate = approachRate });
|
||||||
|
return hitCircle.TimePreempt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,8 +68,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
public void OnSliderTrackingChange(ValueChangedEvent<bool> e)
|
public void OnSliderTrackingChange(ValueChangedEvent<bool> e)
|
||||||
{
|
{
|
||||||
// If a slider is in a tracking state, a further dim should be applied to the (remaining) visible portion of the playfield over a brief duration.
|
// If a slider is in a tracking state, a further dim should be applied to the (remaining) visible portion of the playfield.
|
||||||
this.TransformTo(nameof(FlashlightDim), e.NewValue ? 0.8f : 0.0f, 50);
|
FlashlightDim = e.NewValue ? 0.8f : 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||||
|
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
ComboOffset = original.ComboOffset;
|
ComboOffset = original.ComboOffset;
|
||||||
LegacyLastTickOffset = original.LegacyLastTickOffset;
|
LegacyLastTickOffset = original.LegacyLastTickOffset;
|
||||||
TickDistanceMultiplier = original.TickDistanceMultiplier;
|
TickDistanceMultiplier = original.TickDistanceMultiplier;
|
||||||
SliderVelocity = original.SliderVelocity;
|
SliderVelocityMultiplier = original.SliderVelocityMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
|
@ -18,6 +18,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
|||||||
using osu.Game.Rulesets.Osu.Judgements;
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
using osu.Game.Rulesets.Osu.Skinning;
|
using osu.Game.Rulesets.Osu.Skinning;
|
||||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -154,12 +155,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
}
|
}
|
||||||
|
|
||||||
var result = ResultFor(timeOffset);
|
var result = ResultFor(timeOffset);
|
||||||
|
var clickAction = CheckHittable?.Invoke(this, Time.Current, result);
|
||||||
|
|
||||||
if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
|
if (clickAction == ClickAction.Shake)
|
||||||
{
|
|
||||||
Shake();
|
Shake();
|
||||||
|
|
||||||
|
if (result == HitResult.None || clickAction != ClickAction.Hit)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
ApplyResult(r =>
|
ApplyResult(r =>
|
||||||
{
|
{
|
||||||
|
@ -12,6 +12,8 @@ using osu.Game.Rulesets.Judgements;
|
|||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Judgements;
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
using osu.Game.Rulesets.Osu.Scoring;
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -30,10 +32,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
protected override float SamplePlaybackPosition => CalculateDrawableRelativePosition(this);
|
protected override float SamplePlaybackPosition => CalculateDrawableRelativePosition(this);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this <see cref="DrawableOsuHitObject"/> can be hit, given a time value.
|
/// What action this <see cref="DrawableOsuHitObject"/> should take in response to a
|
||||||
/// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false.
|
/// click at the given time value.
|
||||||
|
/// If non-null, judgements will be ignored for return values of <see cref="ClickAction.Ignore"/>
|
||||||
|
/// and <see cref="ClickAction.Shake"/>, and this hit object will be shaken for return values of
|
||||||
|
/// <see cref="ClickAction.Shake"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<DrawableHitObject, double, bool> CheckHittable;
|
public Func<DrawableHitObject, double, HitResult, ClickAction> CheckHittable;
|
||||||
|
|
||||||
protected DrawableOsuHitObject(OsuHitObject hitObject)
|
protected DrawableOsuHitObject(OsuHitObject hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
|
@ -8,6 +8,7 @@ using System.Diagnostics;
|
|||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
@ -60,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
pathVersion.BindTo(DrawableSlider.PathVersion);
|
pathVersion.BindTo(DrawableSlider.PathVersion);
|
||||||
|
|
||||||
CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? true;
|
CheckHittable = (d, t, r) => DrawableSlider.CheckHittable?.Invoke(d, t, r) ?? ClickAction.Hit;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
|
@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
public double SpanDuration => Duration / this.SpanCount();
|
public double SpanDuration => Duration / this.SpanCount();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Velocity of this <see cref="Slider"/>.
|
/// The computed velocity of this <see cref="Slider"/>. This is the amount of path distance travelled in 1 ms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double Velocity { get; private set; }
|
public double Velocity { get; private set; }
|
||||||
|
|
||||||
@ -134,17 +134,17 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool OnlyJudgeNestedObjects = true;
|
public bool OnlyJudgeNestedObjects = true;
|
||||||
|
|
||||||
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
|
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
|
||||||
{
|
{
|
||||||
Precision = 0.01,
|
Precision = 0.01,
|
||||||
MinValue = 0.1,
|
MinValue = 0.1,
|
||||||
MaxValue = 10
|
MaxValue = 10
|
||||||
};
|
};
|
||||||
|
|
||||||
public double SliderVelocity
|
public double SliderVelocityMultiplier
|
||||||
{
|
{
|
||||||
get => SliderVelocityBindable.Value;
|
get => SliderVelocityMultiplierBindable.Value;
|
||||||
set => SliderVelocityBindable.Value = value;
|
set => SliderVelocityMultiplierBindable.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool GenerateTicks { get; set; } = true;
|
public bool GenerateTicks { get; set; } = true;
|
||||||
@ -167,7 +167,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
|
|
||||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||||
|
|
||||||
double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * SliderVelocity;
|
double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * SliderVelocityMultiplier;
|
||||||
|
|
||||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||||
TickDistance = GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity;
|
TickDistance = GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity;
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
@ -13,9 +12,9 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class AnyOrderHitPolicy : IHitPolicy
|
public class AnyOrderHitPolicy : IHitPolicy
|
||||||
{
|
{
|
||||||
public IHitObjectContainer HitObjectContainer { get; set; }
|
public IHitObjectContainer HitObjectContainer { get; set; } = null!;
|
||||||
|
|
||||||
public bool IsHittable(DrawableHitObject hitObject, double time) => true;
|
public ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result) => ClickAction.Hit;
|
||||||
|
|
||||||
public void HandleHit(DrawableHitObject hitObject)
|
public void HandleHit(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
|
18
osu.Game.Rulesets.Osu/UI/ClickAction.cs
Normal file
18
osu.Game.Rulesets.Osu/UI/ClickAction.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An action that an <see cref="IHitPolicy"/> recommends be taken in response to a click
|
||||||
|
/// on a <see cref="DrawableOsuHitObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
public enum ClickAction
|
||||||
|
{
|
||||||
|
Ignore,
|
||||||
|
Shake,
|
||||||
|
Hit
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
@ -19,8 +20,9 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to check.</param>
|
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to check.</param>
|
||||||
/// <param name="time">The time to check.</param>
|
/// <param name="time">The time to check.</param>
|
||||||
|
/// <param name="result">The result that the object would be judged with if hit.</param>
|
||||||
/// <returns>Whether <paramref name="hitObject"/> can be hit at the given <paramref name="time"/>.</returns>
|
/// <returns>Whether <paramref name="hitObject"/> can be hit at the given <paramref name="time"/>.</returns>
|
||||||
bool IsHittable(DrawableHitObject hitObject, double time);
|
ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles a <see cref="HitObject"/> being hit.
|
/// Handles a <see cref="HitObject"/> being hit.
|
||||||
|
72
osu.Game.Rulesets.Osu/UI/LegacyHitPolicy.cs
Normal file
72
osu.Game.Rulesets.Osu/UI/LegacyHitPolicy.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures that <see cref="HitObject"/>s are hit in order of appearance. The classic note lock.
|
||||||
|
/// <remarks>
|
||||||
|
/// Hits will be blocked until the previous <see cref="HitObject"/>s have been judged.
|
||||||
|
/// </remarks>
|
||||||
|
/// </summary>
|
||||||
|
public class LegacyHitPolicy : IHitPolicy
|
||||||
|
{
|
||||||
|
public IHitObjectContainer? HitObjectContainer { get; set; }
|
||||||
|
|
||||||
|
private readonly double hittableRange;
|
||||||
|
|
||||||
|
public LegacyHitPolicy(double hittableRange = OsuHitWindows.MISS_WINDOW)
|
||||||
|
{
|
||||||
|
this.hittableRange = hittableRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HandleHit(DrawableHitObject hitObject)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result)
|
||||||
|
{
|
||||||
|
if (HitObjectContainer == null)
|
||||||
|
throw new InvalidOperationException($"{nameof(HitObjectContainer)} should be set before {nameof(CheckHittable)} is called.");
|
||||||
|
|
||||||
|
var aliveObjects = HitObjectContainer.AliveObjects.ToList();
|
||||||
|
int index = aliveObjects.IndexOf(hitObject);
|
||||||
|
|
||||||
|
if (index > 0)
|
||||||
|
{
|
||||||
|
var previousHitObject = (DrawableOsuHitObject)aliveObjects[index - 1];
|
||||||
|
if (previousHitObject.HitObject.StackHeight > 0 && !previousHitObject.AllJudged)
|
||||||
|
return ClickAction.Ignore;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == HitResult.None)
|
||||||
|
return ClickAction.Shake;
|
||||||
|
|
||||||
|
foreach (DrawableHitObject testObject in aliveObjects)
|
||||||
|
{
|
||||||
|
if (testObject.AllJudged)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// if we found the object being checked, we can move on to the final timing test.
|
||||||
|
if (testObject == hitObject)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// for all other objects, we check for validity and block the hit if any are still valid.
|
||||||
|
// 3ms of extra leniency to account for slightly unsnapped objects.
|
||||||
|
if (testObject.HitObject.GetEndTime() + 3 < hitObject.HitObject.StartTime)
|
||||||
|
return ClickAction.Shake;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.Abs(hitObject.HitObject.StartTime - time) < hittableRange ? ClickAction.Hit : ClickAction.Shake;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,56 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Game.Rulesets.Objects;
|
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
|
||||||
using osu.Game.Rulesets.UI;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Ensures that <see cref="HitObject"/>s are hit in order of appearance. The classic note lock.
|
|
||||||
/// <remarks>
|
|
||||||
/// Hits will be blocked until the previous <see cref="HitObject"/>s have been judged.
|
|
||||||
/// </remarks>
|
|
||||||
/// </summary>
|
|
||||||
public class ObjectOrderedHitPolicy : IHitPolicy
|
|
||||||
{
|
|
||||||
public IHitObjectContainer HitObjectContainer { get; set; }
|
|
||||||
|
|
||||||
public bool IsHittable(DrawableHitObject hitObject, double time) => enumerateHitObjectsUpTo(hitObject.HitObject.StartTime).All(obj => obj.AllJudged);
|
|
||||||
|
|
||||||
public void HandleHit(DrawableHitObject hitObject)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
|
|
||||||
{
|
|
||||||
foreach (var obj in HitObjectContainer.AliveObjects)
|
|
||||||
{
|
|
||||||
if (obj.HitObject.StartTime >= targetTime)
|
|
||||||
yield break;
|
|
||||||
|
|
||||||
switch (obj)
|
|
||||||
{
|
|
||||||
case DrawableSpinner:
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case DrawableSlider slider:
|
|
||||||
yield return slider.HeadCircle;
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
yield return obj;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
|
|
||||||
protected override void OnNewDrawableHitObject(DrawableHitObject drawable)
|
protected override void OnNewDrawableHitObject(DrawableHitObject drawable)
|
||||||
{
|
{
|
||||||
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable;
|
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.CheckHittable;
|
||||||
|
|
||||||
Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}");
|
Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}");
|
||||||
drawable.OnLoadComplete += onDrawableHitObjectLoaded;
|
drawable.OnLoadComplete += onDrawableHitObjectLoaded;
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
@ -22,11 +21,14 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class StartTimeOrderedHitPolicy : IHitPolicy
|
public class StartTimeOrderedHitPolicy : IHitPolicy
|
||||||
{
|
{
|
||||||
public IHitObjectContainer HitObjectContainer { get; set; }
|
public IHitObjectContainer? HitObjectContainer { get; set; }
|
||||||
|
|
||||||
public bool IsHittable(DrawableHitObject hitObject, double time)
|
public ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult _)
|
||||||
{
|
{
|
||||||
DrawableHitObject blockingObject = null;
|
if (HitObjectContainer == null)
|
||||||
|
throw new InvalidOperationException($"{nameof(HitObjectContainer)} should be set before {nameof(CheckHittable)} is called.");
|
||||||
|
|
||||||
|
DrawableHitObject? blockingObject = null;
|
||||||
|
|
||||||
foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
|
foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
|
||||||
{
|
{
|
||||||
@ -36,22 +38,25 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
|
|
||||||
// If there is no previous hitobject, allow the hit.
|
// If there is no previous hitobject, allow the hit.
|
||||||
if (blockingObject == null)
|
if (blockingObject == null)
|
||||||
return true;
|
return ClickAction.Hit;
|
||||||
|
|
||||||
// A hit is allowed if:
|
// A hit is allowed if:
|
||||||
// 1. The last blocking hitobject has been judged.
|
// 1. The last blocking hitobject has been judged.
|
||||||
// 2. The current time is after the last hitobject's start time.
|
// 2. The current time is after the last hitobject's start time.
|
||||||
// Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245).
|
// Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245).
|
||||||
return blockingObject.Judged || time >= blockingObject.HitObject.StartTime;
|
return (blockingObject.Judged || time >= blockingObject.HitObject.StartTime) ? ClickAction.Hit : ClickAction.Shake;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void HandleHit(DrawableHitObject hitObject)
|
public void HandleHit(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
|
if (HitObjectContainer == null)
|
||||||
|
throw new InvalidOperationException($"{nameof(HitObjectContainer)} should be set before {nameof(HandleHit)} is called.");
|
||||||
|
|
||||||
// Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners).
|
// Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners).
|
||||||
if (!hitObjectCanBlockFutureHits(hitObject))
|
if (!hitObjectCanBlockFutureHits(hitObject))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset))
|
if (CheckHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset, hitObject.Result.Type) != ClickAction.Hit)
|
||||||
throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
|
throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
|
||||||
|
|
||||||
// Miss all hitobjects prior to the hit one.
|
// Miss all hitobjects prior to the hit one.
|
||||||
@ -74,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
|
|
||||||
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
|
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
|
||||||
{
|
{
|
||||||
foreach (var obj in HitObjectContainer.AliveObjects)
|
foreach (var obj in HitObjectContainer!.AliveObjects)
|
||||||
{
|
{
|
||||||
if (obj.HitObject.StartTime >= targetTime)
|
if (obj.HitObject.StartTime >= targetTime)
|
||||||
yield break;
|
yield break;
|
||||||
|
@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
|||||||
{
|
{
|
||||||
if (hitObject is not IHasSliderVelocity hasSliderVelocity) continue;
|
if (hitObject is not IHasSliderVelocity hasSliderVelocity) continue;
|
||||||
|
|
||||||
double nextScrollSpeed = hasSliderVelocity.SliderVelocity;
|
double nextScrollSpeed = hasSliderVelocity.SliderVelocityMultiplier;
|
||||||
EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime);
|
EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime);
|
||||||
|
|
||||||
if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision))
|
if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision))
|
||||||
@ -189,7 +189,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
|||||||
if (obj.LegacyBpmMultiplier.HasValue)
|
if (obj.LegacyBpmMultiplier.HasValue)
|
||||||
beatLength = timingPoint.BeatLength * obj.LegacyBpmMultiplier.Value;
|
beatLength = timingPoint.BeatLength * obj.LegacyBpmMultiplier.Value;
|
||||||
else if (obj is IHasSliderVelocity hasSliderVelocity)
|
else if (obj is IHasSliderVelocity hasSliderVelocity)
|
||||||
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
|
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocityMultiplier;
|
||||||
else
|
else
|
||||||
beatLength = timingPoint.BeatLength;
|
beatLength = timingPoint.BeatLength;
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
assertSnapDistance(100, new Slider
|
assertSnapDistance(100, new Slider
|
||||||
{
|
{
|
||||||
SliderVelocity = multiplier
|
SliderVelocityMultiplier = multiplier
|
||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
assertSnapDistance(100 * multiplier, new Slider
|
assertSnapDistance(100 * multiplier, new Slider
|
||||||
{
|
{
|
||||||
SliderVelocity = multiplier
|
SliderVelocityMultiplier = multiplier
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
|
|
||||||
var referenceObject = new Slider
|
var referenceObject = new Slider
|
||||||
{
|
{
|
||||||
SliderVelocity = slider_velocity
|
SliderVelocityMultiplier = slider_velocity
|
||||||
};
|
};
|
||||||
|
|
||||||
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
|
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
|
||||||
|
@ -47,7 +47,35 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
|||||||
pill.AutoSizeAxes = Axes.Y;
|
pill.AutoSizeAxes = Axes.Y;
|
||||||
pill.Width = 90;
|
pill.Width = 90;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
AddStep("unset fixed width", () => statusPills.ForEach(pill => pill.AutoSizeAxes = Axes.Both));
|
AddStep("unset fixed width", () => statusPills.ForEach(pill => pill.AutoSizeAxes = Axes.Both));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestChangeLabels()
|
||||||
|
{
|
||||||
|
AddStep("Change labels", () =>
|
||||||
|
{
|
||||||
|
foreach (var pill in this.ChildrenOfType<BeatmapSetOnlineStatusPill>())
|
||||||
|
{
|
||||||
|
switch (pill.Status)
|
||||||
|
{
|
||||||
|
// cycle at end
|
||||||
|
case BeatmapOnlineStatus.Loved:
|
||||||
|
pill.Status = BeatmapOnlineStatus.LocallyModified;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// skip none
|
||||||
|
case BeatmapOnlineStatus.LocallyModified:
|
||||||
|
pill.Status = BeatmapOnlineStatus.Graveyard;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
pill.Status = (pill.Status + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
new PathControlPoint(new Vector2(100, 0))
|
new PathControlPoint(new Vector2(100, 0))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SliderVelocity = 2
|
SliderVelocityMultiplier = 2
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddStep("unify slider velocity", () =>
|
AddStep("unify slider velocity", () =>
|
||||||
{
|
{
|
||||||
foreach (var h in EditorBeatmap.HitObjects.OfType<IHasSliderVelocity>())
|
foreach (var h in EditorBeatmap.HitObjects.OfType<IHasSliderVelocity>())
|
||||||
h.SliderVelocity = 1.5;
|
h.SliderVelocityMultiplier = 1.5;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||||
@ -194,7 +194,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () =>
|
private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () =>
|
||||||
{
|
{
|
||||||
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
|
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
|
||||||
return h is IHasSliderVelocity hasSliderVelocity && hasSliderVelocity.SliderVelocity == velocity;
|
return h is IHasSliderVelocity hasSliderVelocity && hasSliderVelocity.SliderVelocityMultiplier == velocity;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ using osu.Framework.Extensions.ObjectExtensions;
|
|||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Rulesets.Mania;
|
using osu.Game.Rulesets.Mania;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
@ -16,6 +17,7 @@ using osu.Game.Screens.Edit;
|
|||||||
using osu.Game.Screens.Edit.GameplayTest;
|
using osu.Game.Screens.Edit.GameplayTest;
|
||||||
using osu.Game.Screens.Menu;
|
using osu.Game.Screens.Menu;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
|
using osu.Game.Screens.Select.Filter;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
@ -203,6 +205,33 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying);
|
AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase(SortMode.Title)]
|
||||||
|
[TestCase(SortMode.Difficulty)]
|
||||||
|
public void TestSelectionRetainedOnExit(SortMode sortMode)
|
||||||
|
{
|
||||||
|
BeatmapSetInfo beatmapSet = null!;
|
||||||
|
|
||||||
|
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||||
|
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||||
|
|
||||||
|
AddStep($"set sort mode to {sortMode}", () => Game.LocalConfig.SetValue(OsuSetting.SongSelectSortingMode, sortMode));
|
||||||
|
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||||
|
AddUntilStep("wait for song select",
|
||||||
|
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||||
|
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||||
|
&& songSelect.IsLoaded);
|
||||||
|
|
||||||
|
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||||
|
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||||
|
|
||||||
|
AddStep("exit editor", () => InputManager.Key(Key.Escape));
|
||||||
|
AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor);
|
||||||
|
|
||||||
|
AddUntilStep("selection retained on song select",
|
||||||
|
() => Game.Beatmap.Value.BeatmapInfo.ID,
|
||||||
|
() => Is.EqualTo(beatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0).ID));
|
||||||
|
}
|
||||||
|
|
||||||
private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType<EditorBeatmap>().Single();
|
private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType<EditorBeatmap>().Single();
|
||||||
|
|
||||||
private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;
|
private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;
|
||||||
|
@ -134,14 +134,16 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
|
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
|
||||||
Description = "Outstanding help by being a voluntary test subject.",
|
Description = "Outstanding help by being a voluntary test subject.",
|
||||||
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.jpg",
|
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor-new@2x.png",
|
||||||
|
ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor-new.png",
|
||||||
Url = "https://osu.ppy.sh/wiki/en/People/Community_Contributors",
|
Url = "https://osu.ppy.sh/wiki/en/People/Community_Contributors",
|
||||||
},
|
},
|
||||||
new Badge
|
new Badge
|
||||||
{
|
{
|
||||||
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
|
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
|
||||||
Description = "Badge without a url.",
|
Description = "Badge without a url.",
|
||||||
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.jpg",
|
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor@2x.png",
|
||||||
|
ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor.png",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Title = "osu!volunteer",
|
Title = "osu!volunteer",
|
||||||
|
@ -39,6 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
private BeatmapInfo currentSelection => carousel.SelectedBeatmapInfo;
|
private BeatmapInfo currentSelection => carousel.SelectedBeatmapInfo;
|
||||||
|
|
||||||
private const int set_count = 5;
|
private const int set_count = 5;
|
||||||
|
private const int diff_count = 3;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(RulesetStore rulesets)
|
private void load(RulesetStore rulesets)
|
||||||
@ -111,7 +112,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestScrollPositionMaintainedOnAdd()
|
public void TestScrollPositionMaintainedOnAdd()
|
||||||
{
|
{
|
||||||
loadBeatmaps(count: 1, randomDifficulties: false);
|
loadBeatmaps(setCount: 1);
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
@ -124,7 +125,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestDeletion()
|
public void TestDeletion()
|
||||||
{
|
{
|
||||||
loadBeatmaps(count: 5, randomDifficulties: true);
|
loadBeatmaps(setCount: 5, randomDifficulties: true);
|
||||||
|
|
||||||
AddStep("remove first set", () => carousel.RemoveBeatmapSet(carousel.Items.Select(item => item.Item).OfType<CarouselBeatmapSet>().First().BeatmapSet));
|
AddStep("remove first set", () => carousel.RemoveBeatmapSet(carousel.Items.Select(item => item.Item).OfType<CarouselBeatmapSet>().First().BeatmapSet));
|
||||||
AddUntilStep("4 beatmap sets visible", () => this.ChildrenOfType<DrawableCarouselBeatmapSet>().Count(set => set.Alpha > 0) == 4);
|
AddUntilStep("4 beatmap sets visible", () => this.ChildrenOfType<DrawableCarouselBeatmapSet>().Count(set => set.Alpha > 0) == 4);
|
||||||
@ -133,7 +134,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestScrollPositionMaintainedOnDelete()
|
public void TestScrollPositionMaintainedOnDelete()
|
||||||
{
|
{
|
||||||
loadBeatmaps(count: 50, randomDifficulties: false);
|
loadBeatmaps(setCount: 50);
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
@ -150,7 +151,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestManyPanels()
|
public void TestManyPanels()
|
||||||
{
|
{
|
||||||
loadBeatmaps(count: 5000, randomDifficulties: true);
|
loadBeatmaps(setCount: 5000, randomDifficulties: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -501,6 +502,34 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
waitForSelection(set_count);
|
waitForSelection(set_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddRemoveDifficultySort()
|
||||||
|
{
|
||||||
|
const int local_set_count = 2;
|
||||||
|
const int local_diff_count = 2;
|
||||||
|
|
||||||
|
loadBeatmaps(setCount: local_set_count, diffCount: local_diff_count);
|
||||||
|
|
||||||
|
AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false));
|
||||||
|
|
||||||
|
checkVisibleItemCount(false, local_set_count * local_diff_count);
|
||||||
|
|
||||||
|
var firstAdded = TestResources.CreateTestBeatmapSetInfo(local_diff_count);
|
||||||
|
firstAdded.Status = BeatmapOnlineStatus.Loved;
|
||||||
|
|
||||||
|
AddStep("Add new set", () => carousel.UpdateBeatmapSet(firstAdded));
|
||||||
|
|
||||||
|
checkVisibleItemCount(false, (local_set_count + 1) * local_diff_count);
|
||||||
|
|
||||||
|
AddStep("Remove set", () => carousel.RemoveBeatmapSet(firstAdded));
|
||||||
|
|
||||||
|
checkVisibleItemCount(false, (local_set_count) * local_diff_count);
|
||||||
|
|
||||||
|
setSelected(local_set_count, 1);
|
||||||
|
|
||||||
|
waitForSelection(local_set_count);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSelectionEnteringFromEmptyRuleset()
|
public void TestSelectionEnteringFromEmptyRuleset()
|
||||||
{
|
{
|
||||||
@ -662,7 +691,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < 3; i++)
|
||||||
{
|
{
|
||||||
var set = TestResources.CreateTestBeatmapSetInfo(3);
|
var set = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
|
|
||||||
// only need to set the first as they are a shared reference.
|
// only need to set the first as they are a shared reference.
|
||||||
var beatmap = set.Beatmaps.First();
|
var beatmap = set.Beatmaps.First();
|
||||||
@ -709,7 +738,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < 3; i++)
|
||||||
{
|
{
|
||||||
var set = TestResources.CreateTestBeatmapSetInfo(3);
|
var set = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
|
|
||||||
// only need to set the first as they are a shared reference.
|
// only need to set the first as they are a shared reference.
|
||||||
var beatmap = set.Beatmaps.First();
|
var beatmap = set.Beatmaps.First();
|
||||||
@ -758,32 +787,54 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSortingWithFiltered()
|
public void TestSortingWithDifficultyFiltered()
|
||||||
{
|
{
|
||||||
|
const int local_diff_count = 3;
|
||||||
|
const int local_set_count = 2;
|
||||||
|
|
||||||
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
|
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
AddStep("Populuate beatmap sets", () =>
|
AddStep("Populuate beatmap sets", () =>
|
||||||
{
|
{
|
||||||
sets.Clear();
|
sets.Clear();
|
||||||
|
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < local_set_count; i++)
|
||||||
{
|
{
|
||||||
var set = TestResources.CreateTestBeatmapSetInfo(3);
|
var set = TestResources.CreateTestBeatmapSetInfo(local_diff_count);
|
||||||
set.Beatmaps[0].StarRating = 3 - i;
|
set.Beatmaps[0].StarRating = 3 - i;
|
||||||
set.Beatmaps[2].StarRating = 6 + i;
|
set.Beatmaps[1].StarRating = 6 + i;
|
||||||
sets.Add(set);
|
sets.Add(set);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
loadBeatmaps(sets);
|
loadBeatmaps(sets);
|
||||||
|
|
||||||
|
AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false));
|
||||||
|
|
||||||
|
checkVisibleItemCount(false, local_set_count * local_diff_count);
|
||||||
|
checkVisibleItemCount(true, 1);
|
||||||
|
|
||||||
AddStep("Filter to normal", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Normal" }, false));
|
AddStep("Filter to normal", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Normal" }, false));
|
||||||
AddAssert("Check first set at end", () => carousel.BeatmapSets.First().Equals(sets.Last()));
|
checkVisibleItemCount(false, local_set_count);
|
||||||
AddAssert("Check last set at start", () => carousel.BeatmapSets.Last().Equals(sets.First()));
|
checkVisibleItemCount(true, 1);
|
||||||
|
|
||||||
|
AddUntilStep("Check all visible sets have one normal", () =>
|
||||||
|
{
|
||||||
|
return carousel.Items.OfType<DrawableCarouselBeatmapSet>()
|
||||||
|
.Where(p => p.IsPresent)
|
||||||
|
.Count(p => ((CarouselBeatmapSet)p.Item)!.Beatmaps.Single().BeatmapInfo.DifficultyName.StartsWith("Normal", StringComparison.Ordinal)) == local_set_count;
|
||||||
|
});
|
||||||
|
|
||||||
AddStep("Filter to insane", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Insane" }, false));
|
AddStep("Filter to insane", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Insane" }, false));
|
||||||
AddAssert("Check first set at start", () => carousel.BeatmapSets.First().Equals(sets.First()));
|
checkVisibleItemCount(false, local_set_count);
|
||||||
AddAssert("Check last set at end", () => carousel.BeatmapSets.Last().Equals(sets.Last()));
|
checkVisibleItemCount(true, 1);
|
||||||
|
|
||||||
|
AddUntilStep("Check all visible sets have one insane", () =>
|
||||||
|
{
|
||||||
|
return carousel.Items.OfType<DrawableCarouselBeatmapSet>()
|
||||||
|
.Where(p => p.IsPresent)
|
||||||
|
.Count(p => ((CarouselBeatmapSet)p.Item)!.Beatmaps.Single().BeatmapInfo.DifficultyName.StartsWith("Insane", StringComparison.Ordinal)) == local_set_count;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -838,7 +889,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
AddStep("create hidden set", () =>
|
AddStep("create hidden set", () =>
|
||||||
{
|
{
|
||||||
hidingSet = TestResources.CreateTestBeatmapSetInfo(3);
|
hidingSet = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
hidingSet.Beatmaps[1].Hidden = true;
|
hidingSet.Beatmaps[1].Hidden = true;
|
||||||
|
|
||||||
hiddenList.Clear();
|
hiddenList.Clear();
|
||||||
@ -885,7 +936,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
AddStep("add mixed ruleset beatmapset", () =>
|
AddStep("add mixed ruleset beatmapset", () =>
|
||||||
{
|
{
|
||||||
testMixed = TestResources.CreateTestBeatmapSetInfo(3);
|
testMixed = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
|
|
||||||
for (int i = 0; i <= 2; i++)
|
for (int i = 0; i <= 2; i++)
|
||||||
{
|
{
|
||||||
@ -907,7 +958,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
BeatmapSetInfo testSingle = null;
|
BeatmapSetInfo testSingle = null;
|
||||||
AddStep("add single ruleset beatmapset", () =>
|
AddStep("add single ruleset beatmapset", () =>
|
||||||
{
|
{
|
||||||
testSingle = TestResources.CreateTestBeatmapSetInfo(3);
|
testSingle = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
testSingle.Beatmaps.ForEach(b =>
|
testSingle.Beatmaps.ForEach(b =>
|
||||||
{
|
{
|
||||||
b.Ruleset = rulesets.AvailableRulesets.ElementAt(1);
|
b.Ruleset = rulesets.AvailableRulesets.ElementAt(1);
|
||||||
@ -930,7 +981,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
manySets.Clear();
|
manySets.Clear();
|
||||||
|
|
||||||
for (int i = 1; i <= 50; i++)
|
for (int i = 1; i <= 50; i++)
|
||||||
manySets.Add(TestResources.CreateTestBeatmapSetInfo(3));
|
manySets.Add(TestResources.CreateTestBeatmapSetInfo(diff_count));
|
||||||
});
|
});
|
||||||
|
|
||||||
loadBeatmaps(manySets);
|
loadBeatmaps(manySets);
|
||||||
@ -955,6 +1006,43 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1);
|
AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCarouselRemembersSelectionDifficultySort()
|
||||||
|
{
|
||||||
|
List<BeatmapSetInfo> manySets = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
|
AddStep("Populate beatmap sets", () =>
|
||||||
|
{
|
||||||
|
manySets.Clear();
|
||||||
|
|
||||||
|
for (int i = 1; i <= 50; i++)
|
||||||
|
manySets.Add(TestResources.CreateTestBeatmapSetInfo(diff_count));
|
||||||
|
});
|
||||||
|
|
||||||
|
loadBeatmaps(manySets);
|
||||||
|
|
||||||
|
AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false));
|
||||||
|
|
||||||
|
advanceSelection(direction: 1, diff: false);
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
AddStep("Toggle non-matching filter", () =>
|
||||||
|
{
|
||||||
|
carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("Restore no filter", () =>
|
||||||
|
{
|
||||||
|
carousel.Filter(new FilterCriteria(), false);
|
||||||
|
eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// always returns to same selection as long as it's available.
|
||||||
|
AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestFilteringByUserStarDifficulty()
|
public void TestFilteringByUserStarDifficulty()
|
||||||
{
|
{
|
||||||
@ -1081,20 +1169,26 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null, int? count = null,
|
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null,
|
||||||
bool randomDifficulties = false)
|
int? setCount = null, int? diffCount = null, bool randomDifficulties = false)
|
||||||
{
|
{
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
|
|
||||||
if (beatmapSets == null)
|
if (beatmapSets == null)
|
||||||
{
|
{
|
||||||
beatmapSets = new List<BeatmapSetInfo>();
|
beatmapSets = new List<BeatmapSetInfo>();
|
||||||
|
var statuses = Enum.GetValues<BeatmapOnlineStatus>()
|
||||||
|
.Except(new[] { BeatmapOnlineStatus.None }) // make sure a badge is always shown.
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
for (int i = 1; i <= (count ?? set_count); i++)
|
for (int i = 1; i <= (setCount ?? set_count); i++)
|
||||||
{
|
{
|
||||||
beatmapSets.Add(randomDifficulties
|
var set = randomDifficulties
|
||||||
? TestResources.CreateTestBeatmapSetInfo()
|
? TestResources.CreateTestBeatmapSetInfo()
|
||||||
: TestResources.CreateTestBeatmapSetInfo(3));
|
: TestResources.CreateTestBeatmapSetInfo(diffCount ?? diff_count);
|
||||||
|
set.Status = statuses[RNG.Next(statuses.Length)];
|
||||||
|
|
||||||
|
beatmapSets.Add(set);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Resources.Localisation.Web;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Catch;
|
using osu.Game.Rulesets.Catch;
|
||||||
using osu.Game.Rulesets.Mania;
|
using osu.Game.Rulesets.Mania;
|
||||||
@ -188,7 +189,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
{
|
{
|
||||||
AddUntilStep($"displayed bpm is {target}", () =>
|
AddUntilStep($"displayed bpm is {target}", () =>
|
||||||
{
|
{
|
||||||
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Single(l => l.Statistic.Name == "BPM");
|
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsBpm);
|
||||||
return label.Statistic.Content == target;
|
return label.Statistic.Content == target;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
219
osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs
Normal file
219
osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Screens.Select;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.SongSelect
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public partial class TestSceneBeatmapInfoWedgeV2 : OsuTestScene
|
||||||
|
{
|
||||||
|
private RulesetStore rulesets = null!;
|
||||||
|
private TestBeatmapInfoWedgeV2 infoWedge = null!;
|
||||||
|
private readonly List<IBeatmap> beatmaps = new List<IBeatmap>();
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(RulesetStore rulesets)
|
||||||
|
{
|
||||||
|
this.rulesets = rulesets;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
AddRange(new Drawable[]
|
||||||
|
{
|
||||||
|
// This exists only to make the wedge more visible in the test scene
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Y = -20,
|
||||||
|
Colour = Colour4.Cornsilk.Darken(0.2f),
|
||||||
|
Height = BeatmapInfoWedgeV2.WEDGE_HEIGHT + 40,
|
||||||
|
Width = 0.65f,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Margin = new MarginPadding { Top = 20, Left = -10 }
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Padding = new MarginPadding { Top = 20 },
|
||||||
|
Child = infoWedge = new TestBeatmapInfoWedgeV2
|
||||||
|
{
|
||||||
|
Width = 0.6f,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AddSliderStep("change star difficulty", 0, 11.9, 5.55, v =>
|
||||||
|
{
|
||||||
|
foreach (var hasCurrentValue in infoWedge.ChildrenOfType<IHasCurrentValue<StarDifficulty>>())
|
||||||
|
hasCurrentValue.Current.Value = new StarDifficulty(v, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRulesetChange()
|
||||||
|
{
|
||||||
|
selectBeatmap(Beatmap.Value.Beatmap);
|
||||||
|
|
||||||
|
AddWaitStep("wait for select", 3);
|
||||||
|
|
||||||
|
foreach (var rulesetInfo in rulesets.AvailableRulesets)
|
||||||
|
{
|
||||||
|
var instance = rulesetInfo.CreateInstance();
|
||||||
|
var testBeatmap = createTestBeatmap(rulesetInfo);
|
||||||
|
|
||||||
|
beatmaps.Add(testBeatmap);
|
||||||
|
|
||||||
|
setRuleset(rulesetInfo);
|
||||||
|
|
||||||
|
selectBeatmap(testBeatmap);
|
||||||
|
|
||||||
|
testBeatmapLabels(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestWedgeVisibility()
|
||||||
|
{
|
||||||
|
AddStep("hide", () => { infoWedge.Hide(); });
|
||||||
|
AddWaitStep("wait for hide", 3);
|
||||||
|
AddAssert("check visibility", () => infoWedge.Alpha == 0);
|
||||||
|
AddStep("show", () => { infoWedge.Show(); });
|
||||||
|
AddWaitStep("wait for show", 1);
|
||||||
|
AddAssert("check visibility", () => infoWedge.Alpha > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testBeatmapLabels(Ruleset ruleset)
|
||||||
|
{
|
||||||
|
AddAssert("check title", () => infoWedge.Info!.TitleLabel.Current.Value == $"{ruleset.ShortName}Title");
|
||||||
|
AddAssert("check artist", () => infoWedge.Info!.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist");
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("reset mods", () => SelectedMods.SetDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTruncation()
|
||||||
|
{
|
||||||
|
selectBeatmap(createLongMetadata());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNullBeatmapWithBackground()
|
||||||
|
{
|
||||||
|
selectBeatmap(null);
|
||||||
|
AddAssert("check default title", () => infoWedge.Info!.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title);
|
||||||
|
AddAssert("check default artist", () => infoWedge.Info!.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist);
|
||||||
|
AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setRuleset(RulesetInfo rulesetInfo)
|
||||||
|
{
|
||||||
|
Container? containerBefore = null;
|
||||||
|
|
||||||
|
AddStep("set ruleset", () =>
|
||||||
|
{
|
||||||
|
// wedge content is only refreshed if the ruleset changes, so only wait for load in that case.
|
||||||
|
if (!rulesetInfo.Equals(Ruleset.Value))
|
||||||
|
containerBefore = infoWedge.DisplayedContent;
|
||||||
|
|
||||||
|
Ruleset.Value = rulesetInfo;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectBeatmap(IBeatmap? b)
|
||||||
|
{
|
||||||
|
Container? containerBefore = null;
|
||||||
|
|
||||||
|
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
|
||||||
|
{
|
||||||
|
containerBefore = infoWedge.DisplayedContent;
|
||||||
|
infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b);
|
||||||
|
infoWedge.Show();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IBeatmap createTestBeatmap(RulesetInfo ruleset)
|
||||||
|
{
|
||||||
|
List<HitObject> objects = new List<HitObject>();
|
||||||
|
for (double i = 0; i < 50000; i += 1000)
|
||||||
|
objects.Add(new TestHitObject { StartTime = i });
|
||||||
|
|
||||||
|
return new Beatmap
|
||||||
|
{
|
||||||
|
BeatmapInfo = new BeatmapInfo
|
||||||
|
{
|
||||||
|
Metadata = new BeatmapMetadata
|
||||||
|
{
|
||||||
|
Author = { Username = $"{ruleset.ShortName}Author" },
|
||||||
|
Artist = $"{ruleset.ShortName}Artist",
|
||||||
|
Source = $"{ruleset.ShortName}Source",
|
||||||
|
Title = $"{ruleset.ShortName}Title"
|
||||||
|
},
|
||||||
|
Ruleset = ruleset,
|
||||||
|
StarRating = 6,
|
||||||
|
DifficultyName = $"{ruleset.ShortName}Version",
|
||||||
|
Difficulty = new BeatmapDifficulty()
|
||||||
|
},
|
||||||
|
HitObjects = objects
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private IBeatmap createLongMetadata()
|
||||||
|
{
|
||||||
|
return new Beatmap
|
||||||
|
{
|
||||||
|
BeatmapInfo = new BeatmapInfo
|
||||||
|
{
|
||||||
|
Metadata = new BeatmapMetadata
|
||||||
|
{
|
||||||
|
Author = { Username = "WWWWWWWWWWWWWWW" },
|
||||||
|
Artist = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Artist",
|
||||||
|
Source = "Verrrrry long Source",
|
||||||
|
Title = "Verrrrry long Title"
|
||||||
|
},
|
||||||
|
DifficultyName = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version",
|
||||||
|
Status = BeatmapOnlineStatus.Graveyard,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class TestBeatmapInfoWedgeV2 : BeatmapInfoWedgeV2
|
||||||
|
{
|
||||||
|
public new Container? DisplayedContent => base.DisplayedContent;
|
||||||
|
public new WedgeInfoText? Info => base.Info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestHitObject : ConvertHitObject, IHasPosition
|
||||||
|
{
|
||||||
|
public float X => 0;
|
||||||
|
public float Y => 0;
|
||||||
|
public Vector2 Position { get; } = Vector2.Zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,9 +6,11 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Mods;
|
using osu.Game.Overlays.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.Select.FooterV2;
|
using osu.Game.Screens.Select.FooterV2;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
@ -37,10 +39,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
footer = new FooterV2
|
new PopoverContainer
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Origin = Anchor.Centre
|
Child = footer = new FooterV2(),
|
||||||
},
|
},
|
||||||
overlay = new DummyOverlay()
|
overlay = new DummyOverlay()
|
||||||
};
|
};
|
||||||
@ -56,6 +58,24 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
overlay.Hide();
|
overlay.Hide();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestShowOptions()
|
||||||
|
{
|
||||||
|
AddStep("enable options", () =>
|
||||||
|
{
|
||||||
|
var optionsButton = this.ChildrenOfType<FooterButtonV2>().Last();
|
||||||
|
|
||||||
|
optionsButton.Enabled.Value = true;
|
||||||
|
optionsButton.TriggerClick();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestState()
|
public void TestState()
|
||||||
{
|
{
|
||||||
|
@ -15,6 +15,7 @@ using osu.Game.Overlays;
|
|||||||
using osu.Game.Overlays.Dialog;
|
using osu.Game.Overlays.Dialog;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
using osu.Game.Screens.Select.Carousel;
|
using osu.Game.Screens.Select.Carousel;
|
||||||
|
using osu.Game.Screens.Select.Filter;
|
||||||
using osu.Game.Tests.Online;
|
using osu.Game.Tests.Online;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
@ -192,6 +193,57 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
|
AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSplitDisplay()
|
||||||
|
{
|
||||||
|
ArchiveDownloadRequest<IBeatmapSetInfo>? downloadRequest = null;
|
||||||
|
|
||||||
|
AddStep("set difficulty sort mode", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }));
|
||||||
|
AddStep("update online hash", () =>
|
||||||
|
{
|
||||||
|
testBeatmapSetInfo.Beatmaps.First().OnlineMD5Hash = "different hash";
|
||||||
|
testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
|
||||||
|
|
||||||
|
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("multiple \"sets\" visible", () => carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().Count(), () => Is.GreaterThan(1));
|
||||||
|
AddUntilStep("update button visible", getUpdateButton, () => Is.Not.Null);
|
||||||
|
|
||||||
|
AddStep("click button", () => getUpdateButton()?.TriggerClick());
|
||||||
|
|
||||||
|
AddUntilStep("wait for download started", () =>
|
||||||
|
{
|
||||||
|
downloadRequest = beatmapDownloader.GetExistingDownload(testBeatmapSetInfo);
|
||||||
|
return downloadRequest != null;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for button disabled", () => getUpdateButton()?.Enabled.Value == false);
|
||||||
|
|
||||||
|
AddUntilStep("progress download to completion", () =>
|
||||||
|
{
|
||||||
|
if (downloadRequest is TestSceneOnlinePlayBeatmapAvailabilityTracker.TestDownloadRequest testRequest)
|
||||||
|
{
|
||||||
|
testRequest.SetProgress(testRequest.Progress + 0.1f);
|
||||||
|
|
||||||
|
if (testRequest.Progress >= 1)
|
||||||
|
{
|
||||||
|
testRequest.TriggerSuccess();
|
||||||
|
|
||||||
|
// usually this would be done by the import process.
|
||||||
|
testBeatmapSetInfo.Beatmaps.First().MD5Hash = "different hash";
|
||||||
|
testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
|
||||||
|
|
||||||
|
// usually this would be done by a realm subscription.
|
||||||
|
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private BeatmapCarousel createCarousel()
|
private BeatmapCarousel createCarousel()
|
||||||
{
|
{
|
||||||
return carousel = new BeatmapCarousel
|
return carousel = new BeatmapCarousel
|
||||||
@ -199,7 +251,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
BeatmapSets = new List<BeatmapSetInfo>
|
BeatmapSets = new List<BeatmapSetInfo>
|
||||||
{
|
{
|
||||||
(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo()),
|
(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(5)),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -58,9 +58,14 @@ namespace osu.Game.Tournament.Tests.Components
|
|||||||
|
|
||||||
songBar.Beatmap = new TournamentBeatmap(beatmap);
|
songBar.Beatmap = new TournamentBeatmap(beatmap);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("set mods to HR", () => songBar.Mods = LegacyMods.HardRock);
|
AddStep("set mods to HR", () => songBar.Mods = LegacyMods.HardRock);
|
||||||
AddStep("set mods to DT", () => songBar.Mods = LegacyMods.DoubleTime);
|
AddStep("set mods to DT", () => songBar.Mods = LegacyMods.DoubleTime);
|
||||||
AddStep("unset mods", () => songBar.Mods = LegacyMods.None);
|
AddStep("unset mods", () => songBar.Mods = LegacyMods.None);
|
||||||
|
|
||||||
|
AddToggleStep("toggle expanded", expanded => songBar.Expanded = expanded);
|
||||||
|
|
||||||
|
AddStep("set null beatmap", () => songBar.Beatmap = null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,13 @@ namespace osu.Game.Tournament.Tests.Screens
|
|||||||
{
|
{
|
||||||
public partial class TestSceneScheduleScreen : TournamentScreenTestScene
|
public partial class TestSceneScheduleScreen : TournamentScreenTestScene
|
||||||
{
|
{
|
||||||
|
public override void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("clear matches", () => Ladder.Matches.Clear());
|
||||||
|
|
||||||
|
base.SetUpSteps();
|
||||||
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
@ -34,6 +41,36 @@ namespace osu.Game.Tournament.Tests.Screens
|
|||||||
AddStep("Set null current match", () => Ladder.CurrentMatch.Value = null);
|
AddStep("Set null current match", () => Ladder.CurrentMatch.Value = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUpcomingMatches()
|
||||||
|
{
|
||||||
|
AddStep("Add upcoming match", () =>
|
||||||
|
{
|
||||||
|
var tournamentMatch = CreateSampleMatch();
|
||||||
|
|
||||||
|
tournamentMatch.Date.Value = DateTimeOffset.UtcNow.AddMinutes(5);
|
||||||
|
tournamentMatch.Completed.Value = false;
|
||||||
|
|
||||||
|
Ladder.Matches.Add(tournamentMatch);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRecentMatches()
|
||||||
|
{
|
||||||
|
AddStep("Add recent match", () =>
|
||||||
|
{
|
||||||
|
var tournamentMatch = CreateSampleMatch();
|
||||||
|
|
||||||
|
tournamentMatch.Date.Value = DateTimeOffset.UtcNow;
|
||||||
|
tournamentMatch.Completed.Value = true;
|
||||||
|
tournamentMatch.Team1Score.Value = tournamentMatch.PointsToWin;
|
||||||
|
tournamentMatch.Team2Score.Value = tournamentMatch.PointsToWin / 2;
|
||||||
|
|
||||||
|
Ladder.Matches.Add(tournamentMatch);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void setMatchDate(TimeSpan relativeTime)
|
private void setMatchDate(TimeSpan relativeTime)
|
||||||
// Humanizer cannot handle negative timespans.
|
// Humanizer cannot handle negative timespans.
|
||||||
=> AddStep($"start time is {relativeTime}", () =>
|
=> AddStep($"start time is {relativeTime}", () =>
|
||||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Tournament.Tests
|
|||||||
{
|
{
|
||||||
public TournamentScalingContainer()
|
public TournamentScalingContainer()
|
||||||
{
|
{
|
||||||
TargetDrawSize = new Vector2(1920, 1080);
|
TargetDrawSize = new Vector2(1024, 768);
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,8 +92,16 @@ namespace osu.Game.Tournament.IPC
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
beatmapLookupRequest = new GetBeatmapRequest(new APIBeatmap { OnlineID = beatmapId });
|
beatmapLookupRequest = new GetBeatmapRequest(new APIBeatmap { OnlineID = beatmapId });
|
||||||
beatmapLookupRequest.Success += b => Beatmap.Value = new TournamentBeatmap(b);
|
beatmapLookupRequest.Success += b =>
|
||||||
beatmapLookupRequest.Failure += _ => Beatmap.Value = null;
|
{
|
||||||
|
if (lastBeatmapId == beatmapId)
|
||||||
|
Beatmap.Value = new TournamentBeatmap(b);
|
||||||
|
};
|
||||||
|
beatmapLookupRequest.Failure += _ =>
|
||||||
|
{
|
||||||
|
if (lastBeatmapId == beatmapId)
|
||||||
|
Beatmap.Value = null;
|
||||||
|
};
|
||||||
API.Queue(beatmapLookupRequest);
|
API.Queue(beatmapLookupRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,16 @@ using osu.Framework.Extensions.Color4Extensions;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Tournament
|
namespace osu.Game.Tournament
|
||||||
{
|
{
|
||||||
internal partial class SaveChangesOverlay : CompositeDrawable
|
internal partial class SaveChangesOverlay : CompositeDrawable, IKeyBindingHandler<PlatformAction>
|
||||||
{
|
{
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private TournamentGame tournamentGame { get; set; } = null!;
|
private TournamentGame tournamentGame { get; set; } = null!;
|
||||||
@ -78,6 +81,21 @@ namespace osu.Game.Tournament
|
|||||||
scheduleNextCheck();
|
scheduleNextCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
|
||||||
|
{
|
||||||
|
if (e.Action == PlatformAction.Save && !e.Repeat)
|
||||||
|
{
|
||||||
|
saveChangesButton.TriggerClick();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
private void scheduleNextCheck() => Scheduler.AddDelayed(() => checkForChanges().FireAndForget(), 1000);
|
private void scheduleNextCheck() => Scheduler.AddDelayed(() => checkForChanges().FireAndForget(), 1000);
|
||||||
|
|
||||||
private void saveChanges()
|
private void saveChanges()
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -19,6 +20,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
{
|
{
|
||||||
public partial class ScheduleScreen : TournamentScreen
|
public partial class ScheduleScreen : TournamentScreen
|
||||||
{
|
{
|
||||||
|
private readonly BindableList<TournamentMatch> allMatches = new BindableList<TournamentMatch>();
|
||||||
private readonly Bindable<TournamentMatch?> currentMatch = new Bindable<TournamentMatch?>();
|
private readonly Bindable<TournamentMatch?> currentMatch = new Bindable<TournamentMatch?>();
|
||||||
private Container mainContainer = null!;
|
private Container mainContainer = null!;
|
||||||
private LadderInfo ladder = null!;
|
private LadderInfo ladder = null!;
|
||||||
@ -101,19 +103,34 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
|
allMatches.BindTo(ladder.Matches);
|
||||||
|
allMatches.BindCollectionChanged((_, _) => refresh());
|
||||||
|
|
||||||
currentMatch.BindTo(ladder.CurrentMatch);
|
currentMatch.BindTo(ladder.CurrentMatch);
|
||||||
currentMatch.BindValueChanged(matchChanged, true);
|
currentMatch.BindValueChanged(_ => refresh(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void matchChanged(ValueChangedEvent<TournamentMatch?> match)
|
private void refresh()
|
||||||
{
|
{
|
||||||
var upcoming = ladder.Matches.Where(p => !p.Completed.Value && p.Team1.Value != null && p.Team2.Value != null && Math.Abs(p.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < 4);
|
const int days_for_displays = 4;
|
||||||
var conditionals = ladder
|
|
||||||
.Matches.Where(p => !p.Completed.Value && (p.Team1.Value == null || p.Team2.Value == null) && Math.Abs(p.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < 4)
|
|
||||||
.SelectMany(m => m.ConditionalMatches.Where(cp => m.Acronyms.TrueForAll(a => cp.Acronyms.Contains(a))));
|
|
||||||
|
|
||||||
upcoming = upcoming.Concat(conditionals);
|
IEnumerable<ConditionalTournamentMatch> conditionals =
|
||||||
upcoming = upcoming.OrderBy(p => p.Date.Value).Take(8);
|
allMatches
|
||||||
|
.Where(m => !m.Completed.Value && (m.Team1.Value == null || m.Team2.Value == null) && Math.Abs(m.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < days_for_displays)
|
||||||
|
.SelectMany(m => m.ConditionalMatches.Where(cp => m.Acronyms.TrueForAll(a => cp.Acronyms.Contains(a))));
|
||||||
|
|
||||||
|
IEnumerable<TournamentMatch> upcoming =
|
||||||
|
allMatches
|
||||||
|
.Where(m => !m.Completed.Value && m.Team1.Value != null && m.Team2.Value != null && Math.Abs(m.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < days_for_displays)
|
||||||
|
.Concat(conditionals)
|
||||||
|
.OrderBy(m => m.Date.Value)
|
||||||
|
.Take(8);
|
||||||
|
|
||||||
|
var recent =
|
||||||
|
allMatches
|
||||||
|
.Where(m => m.Completed.Value && m.Team1.Value != null && m.Team2.Value != null && Math.Abs(m.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < days_for_displays)
|
||||||
|
.OrderByDescending(m => m.Date.Value)
|
||||||
|
.Take(8);
|
||||||
|
|
||||||
ScheduleContainer comingUpNext;
|
ScheduleContainer comingUpNext;
|
||||||
|
|
||||||
@ -137,12 +154,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Width = 0.4f,
|
Width = 0.4f,
|
||||||
ChildrenEnumerable = ladder.Matches
|
ChildrenEnumerable = recent.Select(p => new ScheduleMatch(p))
|
||||||
.Where(p => p.Completed.Value && p.Team1.Value != null && p.Team2.Value != null
|
|
||||||
&& Math.Abs(p.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < 4)
|
|
||||||
.OrderByDescending(p => p.Date.Value)
|
|
||||||
.Take(8)
|
|
||||||
.Select(p => new ScheduleMatch(p))
|
|
||||||
},
|
},
|
||||||
new ScheduleContainer("upcoming matches")
|
new ScheduleContainer("upcoming matches")
|
||||||
{
|
{
|
||||||
@ -161,7 +173,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (match.NewValue != null)
|
if (currentMatch.Value != null)
|
||||||
{
|
{
|
||||||
comingUpNext.Child = new FillFlowContainer
|
comingUpNext.Child = new FillFlowContainer
|
||||||
{
|
{
|
||||||
@ -170,12 +182,12 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
Spacing = new Vector2(30),
|
Spacing = new Vector2(30),
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new ScheduleMatch(match.NewValue, false)
|
new ScheduleMatch(currentMatch.Value, false)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
},
|
},
|
||||||
new TournamentSpriteTextWithBackground(match.NewValue.Round.Value?.Name.Value ?? string.Empty)
|
new TournamentSpriteTextWithBackground(currentMatch.Value.Round.Value?.Name.Value ?? string.Empty)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
@ -185,7 +197,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Text = match.NewValue.Team1.Value?.FullName + " vs " + match.NewValue.Team2.Value?.FullName,
|
Text = currentMatch.Value.Team1.Value?.FullName + " vs " + currentMatch.Value.Team2.Value?.FullName,
|
||||||
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold)
|
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold)
|
||||||
},
|
},
|
||||||
new FillFlowContainer
|
new FillFlowContainer
|
||||||
@ -196,7 +208,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new ScheduleMatchDate(match.NewValue.Date.Value)
|
new ScheduleMatchDate(currentMatch.Value.Date.Value)
|
||||||
{
|
{
|
||||||
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular)
|
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular)
|
||||||
}
|
}
|
||||||
@ -282,6 +294,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
{
|
{
|
||||||
Direction = FillDirection.Vertical,
|
Direction = FillDirection.Vertical,
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Spacing = new Vector2(0, -6),
|
||||||
Margin = new MarginPadding(10)
|
Margin = new MarginPadding(10)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Drawing;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -48,8 +47,6 @@ namespace osu.Game.Tournament
|
|||||||
{
|
{
|
||||||
frameworkConfig.BindWith(FrameworkSetting.WindowedSize, windowSize);
|
frameworkConfig.BindWith(FrameworkSetting.WindowedSize, windowSize);
|
||||||
|
|
||||||
windowSize.MinValue = new Size(TournamentSceneManager.REQUIRED_WIDTH, TournamentSceneManager.STREAM_AREA_HEIGHT);
|
|
||||||
|
|
||||||
windowMode = frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
|
windowMode = frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
|
||||||
|
|
||||||
Add(loadingSpinner = new LoadingSpinner(true, true)
|
Add(loadingSpinner = new LoadingSpinner(true, true)
|
||||||
|
@ -19,6 +19,8 @@ namespace osu.Game.Beatmaps.Drawables
|
|||||||
{
|
{
|
||||||
public partial class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip
|
public partial class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip
|
||||||
{
|
{
|
||||||
|
private const double animation_duration = 400;
|
||||||
|
|
||||||
private BeatmapOnlineStatus status;
|
private BeatmapOnlineStatus status;
|
||||||
|
|
||||||
public BeatmapOnlineStatus Status
|
public BeatmapOnlineStatus Status
|
||||||
@ -32,7 +34,12 @@ namespace osu.Game.Beatmaps.Drawables
|
|||||||
status = value;
|
status = value;
|
||||||
|
|
||||||
if (IsLoaded)
|
if (IsLoaded)
|
||||||
|
{
|
||||||
|
AutoSizeDuration = (float)animation_duration;
|
||||||
|
AutoSizeEasing = Easing.OutQuint;
|
||||||
|
|
||||||
updateState();
|
updateState();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,6 +68,8 @@ namespace osu.Game.Beatmaps.Drawables
|
|||||||
{
|
{
|
||||||
Masking = true;
|
Masking = true;
|
||||||
|
|
||||||
|
Alpha = 0;
|
||||||
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
background = new Box
|
background = new Box
|
||||||
@ -83,21 +92,32 @@ namespace osu.Game.Beatmaps.Drawables
|
|||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
updateState();
|
updateState();
|
||||||
|
FinishTransforms(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateState()
|
private void updateState()
|
||||||
{
|
{
|
||||||
Alpha = Status == BeatmapOnlineStatus.None ? 0 : 1;
|
if (Status == BeatmapOnlineStatus.None)
|
||||||
|
{
|
||||||
|
this.FadeOut(animation_duration, Easing.OutQuint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
statusText.Text = Status.GetLocalisableDescription().ToUpper();
|
this.FadeIn(animation_duration, Easing.OutQuint);
|
||||||
|
|
||||||
|
Color4 statusTextColour;
|
||||||
|
|
||||||
if (colourProvider != null)
|
if (colourProvider != null)
|
||||||
statusText.Colour = status == BeatmapOnlineStatus.Graveyard ? colourProvider.Background1 : colourProvider.Background3;
|
statusTextColour = status == BeatmapOnlineStatus.Graveyard ? colourProvider.Background1 : colourProvider.Background3;
|
||||||
else
|
else
|
||||||
statusText.Colour = status == BeatmapOnlineStatus.Graveyard ? colours.GreySeaFoamLight : Color4.Black;
|
statusTextColour = status == BeatmapOnlineStatus.Graveyard ? colours.GreySeaFoamLight : Color4.Black;
|
||||||
|
|
||||||
background.Colour = OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter;
|
statusText.FadeColour(statusTextColour, animation_duration, Easing.OutQuint);
|
||||||
|
background.FadeColour(OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter, animation_duration, Easing.OutQuint);
|
||||||
|
|
||||||
|
statusText.Text = Status.GetLocalisableDescription().ToUpper();
|
||||||
}
|
}
|
||||||
|
|
||||||
public LocalisableString TooltipText
|
public LocalisableString TooltipText
|
||||||
|
@ -111,7 +111,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
||||||
hasSliderVelocity.SliderVelocity = difficultyControlPoint.SliderVelocity;
|
hasSliderVelocity.SliderVelocityMultiplier = difficultyControlPoint.SliderVelocity;
|
||||||
|
|
||||||
hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||||
}
|
}
|
||||||
|
@ -269,7 +269,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
foreach (var hitObject in hitObjects)
|
foreach (var hitObject in hitObjects)
|
||||||
{
|
{
|
||||||
if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
||||||
yield return new DifficultyControlPoint { Time = hitObject.StartTime, SliderVelocity = hasSliderVelocity.SliderVelocity };
|
yield return new DifficultyControlPoint { Time = hitObject.StartTime, SliderVelocity = hasSliderVelocity.SliderVelocityMultiplier };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ using osu.Framework.Localisation;
|
|||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Game.Online;
|
using osu.Game.Online;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
using osu.Game.Localisation;
|
||||||
|
|
||||||
namespace osu.Game.Graphics.Containers
|
namespace osu.Game.Graphics.Containers
|
||||||
{
|
{
|
||||||
@ -74,7 +75,7 @@ namespace osu.Game.Graphics.Containers
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void AddUserLink(IUser user, Action<SpriteText> creationParameters = null)
|
public void AddUserLink(IUser user, Action<SpriteText> creationParameters = null)
|
||||||
=> createLink(CreateChunkFor(user.Username, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user), "view profile");
|
=> createLink(CreateChunkFor(user.Username, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user), ContextMenuStrings.ViewProfile);
|
||||||
|
|
||||||
private void createLink(ITextPart textPart, LinkDetails link, LocalisableString tooltipText, Action action = null)
|
private void createLink(ITextPart textPart, LinkDetails link, LocalisableString tooltipText, Action action = null)
|
||||||
{
|
{
|
||||||
|
@ -30,6 +30,11 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
|
|
||||||
private const float star_spacing = 4;
|
private const float star_spacing = 4;
|
||||||
|
|
||||||
|
public virtual FillDirection Direction
|
||||||
|
{
|
||||||
|
set => stars.Direction = value;
|
||||||
|
}
|
||||||
|
|
||||||
private float current;
|
private float current;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -64,7 +69,6 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
stars = new FillFlowContainer<Star>
|
stars = new FillFlowContainer<Star>
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both,
|
AutoSizeAxes = Axes.Both,
|
||||||
Direction = FillDirection.Horizontal,
|
|
||||||
Spacing = new Vector2(star_spacing),
|
Spacing = new Vector2(star_spacing),
|
||||||
ChildrenEnumerable = Enumerable.Range(0, StarCount).Select(_ => CreateStar())
|
ChildrenEnumerable = Enumerable.Range(0, StarCount).Select(_ => CreateStar())
|
||||||
}
|
}
|
||||||
|
@ -169,6 +169,11 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString RevertToDefault => new TranslatableString(getKey(@"revert_to_default"), @"Revert to default");
|
public static LocalisableString RevertToDefault => new TranslatableString(getKey(@"revert_to_default"), @"Revert to default");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "General"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString General => new TranslatableString(getKey(@"general"), @"General");
|
||||||
|
|
||||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,6 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString DebugSectionHeader => new TranslatableString(getKey(@"debug_section_header"), @"Debug");
|
public static LocalisableString DebugSectionHeader => new TranslatableString(getKey(@"debug_section_header"), @"Debug");
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// "General"
|
|
||||||
/// </summary>
|
|
||||||
public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General");
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Show log overlay"
|
/// "Show log overlay"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -19,11 +19,6 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString BeatmapHeader => new TranslatableString(getKey(@"beatmap_header"), @"Beatmap");
|
public static LocalisableString BeatmapHeader => new TranslatableString(getKey(@"beatmap_header"), @"Beatmap");
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// "General"
|
|
||||||
/// </summary>
|
|
||||||
public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General");
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Audio"
|
/// "Audio"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -9,11 +9,6 @@ namespace osu.Game.Localisation
|
|||||||
{
|
{
|
||||||
private const string prefix = @"osu.Game.Resources.Localisation.GeneralSettings";
|
private const string prefix = @"osu.Game.Resources.Localisation.GeneralSettings";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// "General"
|
|
||||||
/// </summary>
|
|
||||||
public static LocalisableString GeneralSectionHeader => new TranslatableString(getKey(@"general_section_header"), @"General");
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Language"
|
/// "Language"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -19,6 +19,41 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString LocallyModifiedTooltip => new TranslatableString(getKey(@"locally_modified_tooltip"), @"Has been locally modified");
|
public static LocalisableString LocallyModifiedTooltip => new TranslatableString(getKey(@"locally_modified_tooltip"), @"Has been locally modified");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Manage collections"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString ManageCollections => new TranslatableString(getKey(@"manage_collections"), @"Manage collections");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "For all difficulties"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString ForAllDifficulties => new TranslatableString(getKey(@"for_all_difficulties"), @"For all difficulties");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Delete beatmap"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString DeleteBeatmap => new TranslatableString(getKey(@"delete_beatmap"), @"Delete beatmap");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "For selected difficulty"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString ForSelectedDifficulty => new TranslatableString(getKey(@"for_selected_difficulty"), @"For selected difficulty");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Mark as played"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString MarkAsPlayed => new TranslatableString(getKey(@"mark_as_played"), @"Mark as played");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Clear all local scores"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString ClearAllLocalScores => new TranslatableString(getKey(@"clear_all_local_scores"), @"Clear all local scores");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Edit beatmap"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString EditBeatmap => new TranslatableString(getKey(@"edit_beatmap"), @"Edit beatmap");
|
||||||
|
|
||||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,6 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString UserInterfaceSectionHeader => new TranslatableString(getKey(@"user_interface_section_header"), @"User Interface");
|
public static LocalisableString UserInterfaceSectionHeader => new TranslatableString(getKey(@"user_interface_section_header"), @"User Interface");
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// "General"
|
|
||||||
/// </summary>
|
|
||||||
public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General");
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Rotate cursor when dragging"
|
/// "Rotate cursor when dragging"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -14,6 +14,6 @@ namespace osu.Game.Online.API.Requests
|
|||||||
|
|
||||||
protected override string FileExtension => ".osr";
|
protected override string FileExtension => ".osr";
|
||||||
|
|
||||||
protected override string Target => $@"scores/{Model.Ruleset.ShortName}/{Model.OnlineID}/download";
|
protected override string Target => $@"scores/{Model.OnlineID}/download";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,9 @@ namespace osu.Game.Online
|
|||||||
WebsiteRootUrl = APIEndpointUrl = @"https://dev.ppy.sh";
|
WebsiteRootUrl = APIEndpointUrl = @"https://dev.ppy.sh";
|
||||||
APIClientSecret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT";
|
APIClientSecret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT";
|
||||||
APIClientID = "5";
|
APIClientID = "5";
|
||||||
SpectatorEndpointUrl = $"{APIEndpointUrl}/spectator";
|
SpectatorEndpointUrl = $@"{APIEndpointUrl}/signalr/spectator";
|
||||||
MultiplayerEndpointUrl = $"{APIEndpointUrl}/multiplayer";
|
MultiplayerEndpointUrl = $@"{APIEndpointUrl}/signalr/multiplayer";
|
||||||
MetadataEndpointUrl = $"{APIEndpointUrl}/metadata";
|
MetadataEndpointUrl = $@"{APIEndpointUrl}/signalr/metadata";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ namespace osu.Game.Overlays.OSD
|
|||||||
public partial class CopyUrlToast : Toast
|
public partial class CopyUrlToast : Toast
|
||||||
{
|
{
|
||||||
public CopyUrlToast()
|
public CopyUrlToast()
|
||||||
: base(UserInterfaceStrings.GeneralHeader, ToastStrings.UrlCopied, "")
|
: base(CommonStrings.General, ToastStrings.UrlCopied, "")
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Profile.Header
|
|||||||
new Box
|
new Box
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Colour = colourProvider.Background5,
|
Colour = colourProvider.Background4,
|
||||||
},
|
},
|
||||||
new Container // artificial shadow
|
new Container // artificial shadow
|
||||||
{
|
{
|
||||||
@ -50,7 +50,7 @@ namespace osu.Game.Overlays.Profile.Header
|
|||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
Spacing = new Vector2(10, 10),
|
Spacing = new Vector2(10, 10),
|
||||||
Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Top = 10 },
|
Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 10 },
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Profile.Header
|
|||||||
new Box
|
new Box
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Colour = colourProvider.Background4,
|
Colour = colourProvider.Background3,
|
||||||
},
|
},
|
||||||
new FillFlowContainer
|
new FillFlowContainer
|
||||||
{
|
{
|
||||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
|
|||||||
{
|
{
|
||||||
public partial class GeneralSettings : SettingsSubsection
|
public partial class GeneralSettings : SettingsSubsection
|
||||||
{
|
{
|
||||||
protected override LocalisableString Header => DebugSettingsStrings.GeneralHeader;
|
protected override LocalisableString Header => CommonStrings.General;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig, IPerformFromScreenRunner? performer)
|
private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig, IPerformFromScreenRunner? performer)
|
||||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
|
|||||||
{
|
{
|
||||||
public partial class GeneralSettings : SettingsSubsection
|
public partial class GeneralSettings : SettingsSubsection
|
||||||
{
|
{
|
||||||
protected override LocalisableString Header => GameplaySettingsStrings.GeneralHeader;
|
protected override LocalisableString Header => CommonStrings.General;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuConfigManager config)
|
private void load(OsuConfigManager config)
|
||||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
|||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private OsuGame? game { get; set; }
|
private OsuGame? game { get; set; }
|
||||||
|
|
||||||
public override LocalisableString Header => GeneralSettingsStrings.GeneralSectionHeader;
|
public override LocalisableString Header => CommonStrings.General;
|
||||||
|
|
||||||
public override Drawable CreateIcon() => new SpriteIcon
|
public override Drawable CreateIcon() => new SpriteIcon
|
||||||
{
|
{
|
||||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
|
|||||||
{
|
{
|
||||||
public partial class GeneralSettings : SettingsSubsection
|
public partial class GeneralSettings : SettingsSubsection
|
||||||
{
|
{
|
||||||
protected override LocalisableString Header => UserInterfaceStrings.GeneralHeader;
|
protected override LocalisableString Header => CommonStrings.General;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuConfigManager config)
|
private void load(OsuConfigManager config)
|
||||||
|
@ -47,10 +47,7 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
|
|
||||||
// copy to mutate, as we will need to compare to the original later on.
|
// copy to mutate, as we will need to compare to the original later on.
|
||||||
var adjustedRect = selectionRect;
|
var adjustedRect = selectionRect;
|
||||||
|
bool isRotated = false;
|
||||||
// first, remove any scale axis we are not interested in.
|
|
||||||
if (anchor.HasFlagFast(Anchor.x1)) scale.X = 0;
|
|
||||||
if (anchor.HasFlagFast(Anchor.y1)) scale.Y = 0;
|
|
||||||
|
|
||||||
// for now aspect lock scale adjustments that occur at corners..
|
// for now aspect lock scale adjustments that occur at corners..
|
||||||
if (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1))
|
if (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1))
|
||||||
@ -61,8 +58,9 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
}
|
}
|
||||||
// ..or if any of the selection have been rotated.
|
// ..or if any of the selection have been rotated.
|
||||||
// this is to avoid requiring skew logic (which would likely not be the user's expected transform anyway).
|
// this is to avoid requiring skew logic (which would likely not be the user's expected transform anyway).
|
||||||
else if (SelectedBlueprints.Any(b => !Precision.AlmostEquals(((Drawable)b.Item).Rotation, 0)))
|
else if (SelectedBlueprints.Any(b => !Precision.AlmostEquals(((Drawable)b.Item).Rotation % 90, 0)))
|
||||||
{
|
{
|
||||||
|
isRotated = true;
|
||||||
if (anchor.HasFlagFast(Anchor.x1))
|
if (anchor.HasFlagFast(Anchor.x1))
|
||||||
// if dragging from the horizontal centre, only a vertical component is available.
|
// if dragging from the horizontal centre, only a vertical component is available.
|
||||||
scale.X = scale.Y / selectionRect.Height * selectionRect.Width;
|
scale.X = scale.Y / selectionRect.Height * selectionRect.Width;
|
||||||
@ -74,13 +72,28 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
if (anchor.HasFlagFast(Anchor.x0)) adjustedRect.X -= scale.X;
|
if (anchor.HasFlagFast(Anchor.x0)) adjustedRect.X -= scale.X;
|
||||||
if (anchor.HasFlagFast(Anchor.y0)) adjustedRect.Y -= scale.Y;
|
if (anchor.HasFlagFast(Anchor.y0)) adjustedRect.Y -= scale.Y;
|
||||||
|
|
||||||
|
// Maintain the selection's centre position if dragging from the centre anchors and selection is rotated.
|
||||||
|
if (isRotated && anchor.HasFlagFast(Anchor.x1)) adjustedRect.X -= scale.X / 2;
|
||||||
|
if (isRotated && anchor.HasFlagFast(Anchor.y1)) adjustedRect.Y -= scale.Y / 2;
|
||||||
|
|
||||||
adjustedRect.Width += scale.X;
|
adjustedRect.Width += scale.X;
|
||||||
adjustedRect.Height += scale.Y;
|
adjustedRect.Height += scale.Y;
|
||||||
|
|
||||||
|
if (adjustedRect.Width <= 0 || adjustedRect.Height <= 0)
|
||||||
|
{
|
||||||
|
Axes toFlip = Axes.None;
|
||||||
|
|
||||||
|
if (adjustedRect.Width <= 0) toFlip |= Axes.X;
|
||||||
|
if (adjustedRect.Height <= 0) toFlip |= Axes.Y;
|
||||||
|
|
||||||
|
SelectionBox.PerformFlipFromScaleHandles(toFlip);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// scale adjust applied to each individual item should match that of the quad itself.
|
// scale adjust applied to each individual item should match that of the quad itself.
|
||||||
var scaledDelta = new Vector2(
|
var scaledDelta = new Vector2(
|
||||||
MathF.Max(adjustedRect.Width / selectionRect.Width, 0),
|
adjustedRect.Width / selectionRect.Width,
|
||||||
MathF.Max(adjustedRect.Height / selectionRect.Height, 0)
|
adjustedRect.Height / selectionRect.Height
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach (var b in SelectedBlueprints)
|
foreach (var b in SelectedBlueprints)
|
||||||
@ -102,7 +115,12 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
);
|
);
|
||||||
|
|
||||||
updateDrawablePosition(drawableItem, newPositionInAdjusted);
|
updateDrawablePosition(drawableItem, newPositionInAdjusted);
|
||||||
drawableItem.Scale *= scaledDelta;
|
|
||||||
|
var currentScaledDelta = scaledDelta;
|
||||||
|
if (Precision.AlmostEquals(MathF.Abs(drawableItem.Rotation) % 180, 90))
|
||||||
|
currentScaledDelta = new Vector2(scaledDelta.Y, scaledDelta.X);
|
||||||
|
|
||||||
|
drawableItem.Scale *= currentScaledDelta;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -291,7 +309,7 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
if (parent == null)
|
if (parent == null)
|
||||||
return drawable.Anchor;
|
return drawable.Anchor;
|
||||||
|
|
||||||
var screenPosition = getScreenPosition();
|
var screenPosition = drawable.ToScreenSpace(drawable.OriginPosition);
|
||||||
|
|
||||||
var absolutePosition = parent.ToLocalSpace(screenPosition);
|
var absolutePosition = parent.ToLocalSpace(screenPosition);
|
||||||
var factor = parent.RelativeToAbsoluteFactor;
|
var factor = parent.RelativeToAbsoluteFactor;
|
||||||
@ -313,26 +331,6 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
result |= getAnchorFromPosition(absolutePosition.Y / factor.Y, Anchor.y0, Anchor.y1, Anchor.y2);
|
result |= getAnchorFromPosition(absolutePosition.Y / factor.Y, Anchor.y0, Anchor.y1, Anchor.y2);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
Vector2 getScreenPosition()
|
|
||||||
{
|
|
||||||
var quad = drawable.ScreenSpaceDrawQuad;
|
|
||||||
var origin = drawable.Origin;
|
|
||||||
|
|
||||||
var pos = quad.TopLeft;
|
|
||||||
|
|
||||||
if (origin.HasFlagFast(Anchor.x2))
|
|
||||||
pos.X += quad.Width;
|
|
||||||
else if (origin.HasFlagFast(Anchor.x1))
|
|
||||||
pos.X += quad.Width / 2f;
|
|
||||||
|
|
||||||
if (origin.HasFlagFast(Anchor.y2))
|
|
||||||
pos.Y += quad.Height;
|
|
||||||
else if (origin.HasFlagFast(Anchor.y1))
|
|
||||||
pos.Y += quad.Height / 2f;
|
|
||||||
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void applyAnchor(Drawable drawable, Anchor anchor)
|
private static void applyAnchor(Drawable drawable, Anchor anchor)
|
||||||
|
@ -244,7 +244,7 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
|
|
||||||
public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true)
|
public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true)
|
||||||
{
|
{
|
||||||
return (float)(100 * (useReferenceSliderVelocity && referenceObject is IHasSliderVelocity hasSliderVelocity ? hasSliderVelocity.SliderVelocity : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1
|
return (float)(100 * (useReferenceSliderVelocity && referenceObject is IHasSliderVelocity hasSliderVelocity ? hasSliderVelocity.SliderVelocityMultiplier : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1
|
||||||
/ BeatSnapProvider.BeatDivisor);
|
/ BeatSnapProvider.BeatDivisor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,14 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
private readonly BindableNumber<float> sliderDisplayCurrent = new BindableNumber<float>();
|
private readonly BindableNumber<float> sliderDisplayCurrent = new BindableNumber<float>();
|
||||||
|
|
||||||
protected override Drawable CreateControl() => new SliderControl(sliderDisplayCurrent);
|
protected sealed override Drawable CreateControl() => new SliderControl(sliderDisplayCurrent, CreateSlider);
|
||||||
|
|
||||||
|
protected virtual RoundedSliderBar<float> CreateSlider(BindableNumber<float> current) => new RoundedSliderBar<float>
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Current = current,
|
||||||
|
KeyboardStep = 0.1f,
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Guards against beatmap values displayed on slider bars being transferred to user override.
|
/// Guards against beatmap values displayed on slider bars being transferred to user override.
|
||||||
@ -100,16 +107,11 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
set => current.Current = value;
|
set => current.Current = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SliderControl(BindableNumber<float> currentNumber)
|
public SliderControl(BindableNumber<float> currentNumber, Func<BindableNumber<float>, RoundedSliderBar<float>> createSlider)
|
||||||
{
|
{
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
new RoundedSliderBar<float>
|
createSlider(currentNumber)
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
Current = currentNumber,
|
|
||||||
KeyboardStep = 0.1f,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
AutoSizeAxes = Axes.Y;
|
AutoSizeAxes = Axes.Y;
|
||||||
|
@ -34,9 +34,18 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
set => CurrentNumber.Precision = value;
|
set => CurrentNumber.Precision = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private float minValue;
|
||||||
|
|
||||||
public float MinValue
|
public float MinValue
|
||||||
{
|
{
|
||||||
set => CurrentNumber.MinValue = value;
|
set
|
||||||
|
{
|
||||||
|
if (value == minValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
minValue = value;
|
||||||
|
updateExtents();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private float maxValue;
|
private float maxValue;
|
||||||
@ -49,7 +58,24 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
maxValue = value;
|
maxValue = value;
|
||||||
updateMaxValue();
|
updateExtents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float? extendedMinValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The minimum value to be used when extended limits are applied.
|
||||||
|
/// </summary>
|
||||||
|
public float? ExtendedMinValue
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == extendedMinValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
extendedMinValue = value;
|
||||||
|
updateExtents();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +92,7 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
extendedMaxValue = value;
|
extendedMaxValue = value;
|
||||||
updateMaxValue();
|
updateExtents();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +104,7 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
public DifficultyBindable(float? defaultValue = null)
|
public DifficultyBindable(float? defaultValue = null)
|
||||||
: base(defaultValue)
|
: base(defaultValue)
|
||||||
{
|
{
|
||||||
ExtendedLimits.BindValueChanged(_ => updateMaxValue());
|
ExtendedLimits.BindValueChanged(_ => updateExtents());
|
||||||
}
|
}
|
||||||
|
|
||||||
public override float? Value
|
public override float? Value
|
||||||
@ -94,8 +120,9 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMaxValue()
|
private void updateExtents()
|
||||||
{
|
{
|
||||||
|
CurrentNumber.MinValue = ExtendedLimits.Value && extendedMinValue != null ? extendedMinValue.Value : minValue;
|
||||||
CurrentNumber.MaxValue = ExtendedLimits.Value && extendedMaxValue != null ? extendedMaxValue.Value : maxValue;
|
CurrentNumber.MaxValue = ExtendedLimits.Value && extendedMaxValue != null ? extendedMaxValue.Value : maxValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,8 +21,9 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
{
|
{
|
||||||
User = new APIUser
|
User = new APIUser
|
||||||
{
|
{
|
||||||
Id = APIUser.SYSTEM_USER_ID,
|
Id = replayData.User.OnlineID,
|
||||||
Username = replayData.User.Username,
|
Username = replayData.User.Username,
|
||||||
|
IsBot = replayData.User.IsBot,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -16,5 +16,6 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
public override ModType Type => ModType.System;
|
public override ModType Type => ModType.System;
|
||||||
public override LocalisableString Description => "Score set on earlier osu! versions with the V2 scoring algorithm active.";
|
public override LocalisableString Description => "Score set on earlier osu! versions with the V2 scoring algorithm active.";
|
||||||
public override double ScoreMultiplier => 1;
|
public override double ScoreMultiplier => 1;
|
||||||
|
public override bool UserPlayable => false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,12 +41,12 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
|||||||
|
|
||||||
public double Velocity = 1;
|
public double Velocity = 1;
|
||||||
|
|
||||||
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1);
|
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1);
|
||||||
|
|
||||||
public double SliderVelocity
|
public double SliderVelocityMultiplier
|
||||||
{
|
{
|
||||||
get => SliderVelocityBindable.Value;
|
get => SliderVelocityMultiplierBindable.Value;
|
||||||
set => SliderVelocityBindable.Value = value;
|
set => SliderVelocityMultiplierBindable.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||||
@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
|||||||
|
|
||||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||||
|
|
||||||
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * SliderVelocity;
|
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * SliderVelocityMultiplier;
|
||||||
|
|
||||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Objects.Types
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The slider velocity multiplier.
|
/// The slider velocity multiplier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
double SliderVelocity { get; set; }
|
double SliderVelocityMultiplier { get; set; }
|
||||||
|
|
||||||
BindableNumber<double> SliderVelocityBindable { get; }
|
BindableNumber<double> SliderVelocityMultiplierBindable { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
if (selected is IHasSliderVelocity sliderVelocity)
|
if (selected is IHasSliderVelocity sliderVelocity)
|
||||||
{
|
{
|
||||||
AddHeader("Slider Velocity");
|
AddHeader("Slider Velocity");
|
||||||
AddValue($"{sliderVelocity.SliderVelocity:#,0.00}x ({sliderVelocity.SliderVelocity * EditorBeatmap.Difficulty.SliderMultiplier:#,0.00}x)");
|
AddValue($"{sliderVelocity.SliderVelocityMultiplier:#,0.00}x ({sliderVelocity.SliderVelocityMultiplier * EditorBeatmap.Difficulty.SliderMultiplier:#,0.00}x)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selected is IHasRepeats repeats)
|
if (selected is IHasRepeats repeats)
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
@ -307,6 +308,25 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// This method should be called when a selection needs to be flipped
|
||||||
|
/// because of an ongoing scale handle drag that would otherwise cause width or height to go negative.
|
||||||
|
/// </remarks>
|
||||||
|
public void PerformFlipFromScaleHandles(Axes axes)
|
||||||
|
{
|
||||||
|
if (axes.HasFlagFast(Axes.X))
|
||||||
|
{
|
||||||
|
dragHandles.FlipScaleHandles(Direction.Horizontal);
|
||||||
|
OnFlip?.Invoke(Direction.Horizontal, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (axes.HasFlagFast(Axes.Y))
|
||||||
|
{
|
||||||
|
dragHandles.FlipScaleHandles(Direction.Vertical);
|
||||||
|
OnFlip?.Invoke(Direction.Vertical, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void addScaleHandle(Anchor anchor)
|
private void addScaleHandle(Anchor anchor)
|
||||||
{
|
{
|
||||||
var handle = new SelectionBoxScaleHandle
|
var handle = new SelectionBoxScaleHandle
|
||||||
|
@ -7,6 +7,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
|
||||||
@ -69,6 +70,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
allDragHandles.Add(handle);
|
allDragHandles.Add(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void FlipScaleHandles(Direction direction)
|
||||||
|
{
|
||||||
|
foreach (var handle in scaleHandles)
|
||||||
|
{
|
||||||
|
if (direction == Direction.Horizontal && !handle.Anchor.HasFlagFast(Anchor.x1))
|
||||||
|
handle.Anchor ^= Anchor.x0 | Anchor.x2;
|
||||||
|
if (direction == Direction.Vertical && !handle.Anchor.HasFlagFast(Anchor.y1))
|
||||||
|
handle.Anchor ^= Anchor.y0 | Anchor.y2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private SelectionBoxRotationHandle displayedRotationHandle;
|
private SelectionBoxRotationHandle displayedRotationHandle;
|
||||||
private SelectionBoxDragHandle activeHandle;
|
private SelectionBoxDragHandle activeHandle;
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
{
|
{
|
||||||
HitObject = hitObject;
|
HitObject = hitObject;
|
||||||
|
|
||||||
speedMultiplier = (hitObject as IHasSliderVelocity)?.SliderVelocityBindable.GetBoundCopy();
|
speedMultiplier = (hitObject as IHasSliderVelocity)?.SliderVelocityMultiplierBindable.GetBoundCopy();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1;
|
protected override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1;
|
||||||
@ -106,8 +106,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).Where(o => o is IHasSliderVelocity).ToArray();
|
var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).Where(o => o is IHasSliderVelocity).ToArray();
|
||||||
|
|
||||||
// even if there are multiple objects selected, we can still display a value if they all have the same value.
|
// even if there are multiple objects selected, we can still display a value if they all have the same value.
|
||||||
var selectedPointBindable = relevantObjects.Select(point => ((IHasSliderVelocity)point).SliderVelocity).Distinct().Count() == 1
|
var selectedPointBindable = relevantObjects.Select(point => ((IHasSliderVelocity)point).SliderVelocityMultiplier).Distinct().Count() == 1
|
||||||
? ((IHasSliderVelocity)relevantObjects.First()).SliderVelocityBindable
|
? ((IHasSliderVelocity)relevantObjects.First()).SliderVelocityMultiplierBindable
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (selectedPointBindable != null)
|
if (selectedPointBindable != null)
|
||||||
@ -127,7 +127,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
|
|
||||||
foreach (var h in relevantObjects)
|
foreach (var h in relevantObjects)
|
||||||
{
|
{
|
||||||
((IHasSliderVelocity)h).SliderVelocity = val.NewValue.Value;
|
((IHasSliderVelocity)h).SliderVelocityMultiplier = val.NewValue.Value;
|
||||||
beatmap.Update(h);
|
beatmap.Update(h);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,7 +169,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
|
|
||||||
InspectorText.Clear();
|
InspectorText.Clear();
|
||||||
|
|
||||||
double[] sliderVelocities = EditorBeatmap.HitObjects.OfType<IHasSliderVelocity>().Select(sv => sv.SliderVelocity).OrderBy(v => v).ToArray();
|
double[] sliderVelocities = EditorBeatmap.HitObjects.OfType<IHasSliderVelocity>().Select(sv => sv.SliderVelocityMultiplier).OrderBy(v => v).ToArray();
|
||||||
|
|
||||||
AddHeader("Base velocity (from beatmap setup)");
|
AddHeader("Base velocity (from beatmap setup)");
|
||||||
AddValue($"{beatmapVelocity:#,0.00}x");
|
AddValue($"{beatmapVelocity:#,0.00}x");
|
||||||
@ -177,6 +177,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
AddHeader("Final velocity");
|
AddHeader("Final velocity");
|
||||||
AddValue($"{beatmapVelocity * current.Value:#,0.00}x");
|
AddValue($"{beatmapVelocity * current.Value:#,0.00}x");
|
||||||
|
|
||||||
|
if (sliderVelocities.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (sliderVelocities.First() != sliderVelocities.Last())
|
if (sliderVelocities.First() != sliderVelocities.Last())
|
||||||
{
|
{
|
||||||
AddHeader("Beatmap velocity range");
|
AddHeader("Beatmap velocity range");
|
||||||
|
@ -395,12 +395,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
|
|
||||||
if (e.CurrentState.Keyboard.ShiftPressed && hitObject is IHasSliderVelocity hasSliderVelocity)
|
if (e.CurrentState.Keyboard.ShiftPressed && hitObject is IHasSliderVelocity hasSliderVelocity)
|
||||||
{
|
{
|
||||||
double newVelocity = hasSliderVelocity.SliderVelocity * (repeatHitObject.Duration / proposedDuration);
|
double newVelocity = hasSliderVelocity.SliderVelocityMultiplier * (repeatHitObject.Duration / proposedDuration);
|
||||||
|
|
||||||
if (Precision.AlmostEquals(newVelocity, hasSliderVelocity.SliderVelocity))
|
if (Precision.AlmostEquals(newVelocity, hasSliderVelocity.SliderVelocityMultiplier))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
hasSliderVelocity.SliderVelocity = newVelocity;
|
hasSliderVelocity.SliderVelocityMultiplier = newVelocity;
|
||||||
beatmap.Update(hitObject);
|
beatmap.Update(hitObject);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -114,7 +114,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (oldObject is IHasSliderVelocity oldWithVelocity && newObject is IHasSliderVelocity newWithVelocity)
|
if (oldObject is IHasSliderVelocity oldWithVelocity && newObject is IHasSliderVelocity newWithVelocity)
|
||||||
oldWithVelocity.SliderVelocity = newWithVelocity.SliderVelocity;
|
oldWithVelocity.SliderVelocityMultiplier = newWithVelocity.SliderVelocityMultiplier;
|
||||||
|
|
||||||
oldObject.Samples = newObject.Samples;
|
oldObject.Samples = newObject.Samples;
|
||||||
|
|
||||||
|
@ -156,7 +156,7 @@ namespace osu.Game.Screens.Ranking
|
|||||||
if (Score != null)
|
if (Score != null)
|
||||||
{
|
{
|
||||||
// only show flair / animation when arriving after watching a play that isn't autoplay.
|
// only show flair / animation when arriving after watching a play that isn't autoplay.
|
||||||
bool shouldFlair = player != null && Score.Mods.All(m => m.UserPlayable);
|
bool shouldFlair = player != null && !Score.User.IsBot;
|
||||||
|
|
||||||
ScorePanelList.AddScore(Score, shouldFlair);
|
ScorePanelList.AddScore(Score, shouldFlair);
|
||||||
}
|
}
|
||||||
|
@ -78,6 +78,8 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
private CarouselBeatmapSet? selectedBeatmapSet;
|
private CarouselBeatmapSet? selectedBeatmapSet;
|
||||||
|
|
||||||
|
private List<BeatmapSetInfo> originalBeatmapSetsDetached = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Raised when the <see cref="SelectedBeatmapInfo"/> is changed.
|
/// Raised when the <see cref="SelectedBeatmapInfo"/> is changed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -127,15 +129,38 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
private void loadBeatmapSets(IEnumerable<BeatmapSetInfo> beatmapSets)
|
private void loadBeatmapSets(IEnumerable<BeatmapSetInfo> beatmapSets)
|
||||||
{
|
{
|
||||||
|
originalBeatmapSetsDetached = beatmapSets.Detach();
|
||||||
|
|
||||||
|
if (selectedBeatmapSet != null && !originalBeatmapSetsDetached.Contains(selectedBeatmapSet.BeatmapSet))
|
||||||
|
selectedBeatmapSet = null;
|
||||||
|
|
||||||
|
var selectedBeatmapBefore = selectedBeatmap?.BeatmapInfo;
|
||||||
|
|
||||||
CarouselRoot newRoot = new CarouselRoot(this);
|
CarouselRoot newRoot = new CarouselRoot(this);
|
||||||
|
|
||||||
newRoot.AddItems(beatmapSets.Select(s => createCarouselSet(s.Detach())).OfType<CarouselBeatmapSet>());
|
if (beatmapsSplitOut)
|
||||||
|
{
|
||||||
|
var carouselBeatmapSets = originalBeatmapSetsDetached.SelectMany(s => s.Beatmaps).Select(b =>
|
||||||
|
{
|
||||||
|
return createCarouselSet(new BeatmapSetInfo(new[] { b })
|
||||||
|
{
|
||||||
|
ID = b.BeatmapSet!.ID,
|
||||||
|
OnlineID = b.BeatmapSet!.OnlineID,
|
||||||
|
Status = b.BeatmapSet!.Status,
|
||||||
|
});
|
||||||
|
}).OfType<CarouselBeatmapSet>();
|
||||||
|
|
||||||
|
newRoot.AddItems(carouselBeatmapSets);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var carouselBeatmapSets = originalBeatmapSetsDetached.Select(createCarouselSet).OfType<CarouselBeatmapSet>();
|
||||||
|
|
||||||
|
newRoot.AddItems(carouselBeatmapSets);
|
||||||
|
}
|
||||||
|
|
||||||
root = newRoot;
|
root = newRoot;
|
||||||
|
|
||||||
if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet))
|
|
||||||
selectedBeatmapSet = null;
|
|
||||||
|
|
||||||
Scroll.Clear(false);
|
Scroll.Clear(false);
|
||||||
itemsCache.Invalidate();
|
itemsCache.Invalidate();
|
||||||
ScrollToSelected();
|
ScrollToSelected();
|
||||||
@ -144,6 +169,15 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
if (loadedTestBeatmaps)
|
if (loadedTestBeatmaps)
|
||||||
signalBeatmapsLoaded();
|
signalBeatmapsLoaded();
|
||||||
|
|
||||||
|
// Restore selection
|
||||||
|
if (selectedBeatmapBefore != null && newRoot.BeatmapSetsByID.TryGetValue(selectedBeatmapBefore.BeatmapSet!.ID, out var newSelectionCandidates))
|
||||||
|
{
|
||||||
|
CarouselBeatmap? found = newSelectionCandidates.SelectMany(s => s.Beatmaps).SingleOrDefault(b => b.BeatmapInfo.ID == selectedBeatmapBefore.ID);
|
||||||
|
|
||||||
|
if (found != null)
|
||||||
|
found.State.Value = CarouselItemState.Selected;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly List<CarouselItem> visibleItems = new List<CarouselItem>();
|
private readonly List<CarouselItem> visibleItems = new List<CarouselItem>();
|
||||||
@ -330,8 +364,8 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
// Only require to action here if the beatmap is missing.
|
// Only require to action here if the beatmap is missing.
|
||||||
// This avoids processing these events unnecessarily when new beatmaps are imported, for example.
|
// This avoids processing these events unnecessarily when new beatmaps are imported, for example.
|
||||||
if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSet)
|
if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSets)
|
||||||
&& existingSet.BeatmapSet.Beatmaps.All(b => b.ID != beatmapInfo.ID))
|
&& existingSets.SelectMany(s => s.Beatmaps).All(b => b.BeatmapInfo.ID != beatmapInfo.ID))
|
||||||
{
|
{
|
||||||
UpdateBeatmapSet(beatmapSet.Detach());
|
UpdateBeatmapSet(beatmapSet.Detach());
|
||||||
}
|
}
|
||||||
@ -345,15 +379,20 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
private void removeBeatmapSet(Guid beatmapSetID) => Schedule(() =>
|
private void removeBeatmapSet(Guid beatmapSetID) => Schedule(() =>
|
||||||
{
|
{
|
||||||
if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSet))
|
if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSets))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach (var beatmap in existingSet.Beatmaps)
|
originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSetID);
|
||||||
randomSelectedBeatmaps.Remove(beatmap);
|
|
||||||
|
|
||||||
previouslyVisitedRandomSets.Remove(existingSet);
|
foreach (var set in existingSets)
|
||||||
|
{
|
||||||
|
foreach (var beatmap in set.Beatmaps)
|
||||||
|
randomSelectedBeatmaps.Remove(beatmap);
|
||||||
|
previouslyVisitedRandomSets.Remove(set);
|
||||||
|
|
||||||
|
root.RemoveItem(set);
|
||||||
|
}
|
||||||
|
|
||||||
root.RemoveItem(existingSet);
|
|
||||||
itemsCache.Invalidate();
|
itemsCache.Invalidate();
|
||||||
|
|
||||||
if (!Scroll.UserScrolling)
|
if (!Scroll.UserScrolling)
|
||||||
@ -366,26 +405,64 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
Guid? previouslySelectedID = null;
|
Guid? previouslySelectedID = null;
|
||||||
|
|
||||||
|
originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSet.ID);
|
||||||
|
originalBeatmapSetsDetached.Add(beatmapSet.Detach());
|
||||||
|
|
||||||
// If the selected beatmap is about to be removed, store its ID so it can be re-selected if required
|
// If the selected beatmap is about to be removed, store its ID so it can be re-selected if required
|
||||||
if (selectedBeatmapSet?.BeatmapSet.ID == beatmapSet.ID)
|
if (selectedBeatmapSet?.BeatmapSet.ID == beatmapSet.ID)
|
||||||
previouslySelectedID = selectedBeatmap?.BeatmapInfo.ID;
|
previouslySelectedID = selectedBeatmap?.BeatmapInfo.ID;
|
||||||
|
|
||||||
var newSet = createCarouselSet(beatmapSet);
|
var removedSets = root.RemoveItemsByID(beatmapSet.ID);
|
||||||
var removedSet = root.RemoveChild(beatmapSet.ID);
|
|
||||||
|
|
||||||
// If we don't remove this here, it may remain in a hidden state until scrolled off screen.
|
foreach (var removedSet in removedSets)
|
||||||
// Doesn't really affect anything during actual user interaction, but makes testing annoying.
|
|
||||||
var removedDrawable = Scroll.FirstOrDefault(c => c.Item == removedSet);
|
|
||||||
if (removedDrawable != null)
|
|
||||||
expirePanelImmediately(removedDrawable);
|
|
||||||
|
|
||||||
if (newSet != null)
|
|
||||||
{
|
{
|
||||||
root.AddItem(newSet);
|
// If we don't remove this here, it may remain in a hidden state until scrolled off screen.
|
||||||
|
// Doesn't really affect anything during actual user interaction, but makes testing annoying.
|
||||||
|
var removedDrawable = Scroll.FirstOrDefault(c => c.Item == removedSet);
|
||||||
|
if (removedDrawable != null)
|
||||||
|
expirePanelImmediately(removedDrawable);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beatmapsSplitOut)
|
||||||
|
{
|
||||||
|
var newSets = new List<CarouselBeatmapSet>();
|
||||||
|
|
||||||
|
foreach (var beatmap in beatmapSet.Beatmaps)
|
||||||
|
{
|
||||||
|
var newSet = createCarouselSet(new BeatmapSetInfo(new[] { beatmap })
|
||||||
|
{
|
||||||
|
ID = beatmapSet.ID,
|
||||||
|
OnlineID = beatmapSet.OnlineID,
|
||||||
|
Status = beatmapSet.Status,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newSet != null)
|
||||||
|
{
|
||||||
|
newSets.Add(newSet);
|
||||||
|
root.AddItem(newSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// check if we can/need to maintain our current selection.
|
// check if we can/need to maintain our current selection.
|
||||||
if (previouslySelectedID != null)
|
if (previouslySelectedID != null)
|
||||||
select((CarouselItem?)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet);
|
{
|
||||||
|
var toSelect = newSets.FirstOrDefault(s => s.Beatmaps.Any(b => b.BeatmapInfo.ID == previouslySelectedID))
|
||||||
|
?? newSets.FirstOrDefault();
|
||||||
|
select(toSelect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var newSet = createCarouselSet(beatmapSet);
|
||||||
|
|
||||||
|
if (newSet != null)
|
||||||
|
{
|
||||||
|
root.AddItem(newSet);
|
||||||
|
|
||||||
|
// check if we can/need to maintain our current selection.
|
||||||
|
if (previouslySelectedID != null)
|
||||||
|
select((CarouselItem?)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
itemsCache.Invalidate();
|
itemsCache.Invalidate();
|
||||||
@ -632,6 +709,8 @@ namespace osu.Game.Screens.Select
|
|||||||
applyActiveCriteria(debounce);
|
applyActiveCriteria(debounce);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool beatmapsSplitOut;
|
||||||
|
|
||||||
private void applyActiveCriteria(bool debounce, bool alwaysResetScrollPosition = true)
|
private void applyActiveCriteria(bool debounce, bool alwaysResetScrollPosition = true)
|
||||||
{
|
{
|
||||||
PendingFilter?.Cancel();
|
PendingFilter?.Cancel();
|
||||||
@ -652,6 +731,13 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
PendingFilter = null;
|
PendingFilter = null;
|
||||||
|
|
||||||
|
if (activeCriteria.SplitOutDifficulties != beatmapsSplitOut)
|
||||||
|
{
|
||||||
|
beatmapsSplitOut = activeCriteria.SplitOutDifficulties;
|
||||||
|
loadBeatmapSets(originalBeatmapSetsDetached);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
root.Filter(activeCriteria);
|
root.Filter(activeCriteria);
|
||||||
itemsCache.Invalidate();
|
itemsCache.Invalidate();
|
||||||
|
|
||||||
@ -1055,7 +1141,7 @@ namespace osu.Game.Screens.Select
|
|||||||
// May only be null during construction (State.Value set causes PerformSelection to be triggered).
|
// May only be null during construction (State.Value set causes PerformSelection to be triggered).
|
||||||
private readonly BeatmapCarousel? carousel;
|
private readonly BeatmapCarousel? carousel;
|
||||||
|
|
||||||
public readonly Dictionary<Guid, CarouselBeatmapSet> BeatmapSetsByID = new Dictionary<Guid, CarouselBeatmapSet>();
|
public readonly Dictionary<Guid, List<CarouselBeatmapSet>> BeatmapSetsByID = new Dictionary<Guid, List<CarouselBeatmapSet>>();
|
||||||
|
|
||||||
public CarouselRoot(BeatmapCarousel carousel)
|
public CarouselRoot(BeatmapCarousel carousel)
|
||||||
{
|
{
|
||||||
@ -1069,20 +1155,25 @@ namespace osu.Game.Screens.Select
|
|||||||
public override void AddItem(CarouselItem i)
|
public override void AddItem(CarouselItem i)
|
||||||
{
|
{
|
||||||
CarouselBeatmapSet set = (CarouselBeatmapSet)i;
|
CarouselBeatmapSet set = (CarouselBeatmapSet)i;
|
||||||
BeatmapSetsByID.Add(set.BeatmapSet.ID, set);
|
if (BeatmapSetsByID.TryGetValue(set.BeatmapSet.ID, out var sets))
|
||||||
|
sets.Add(set);
|
||||||
|
else
|
||||||
|
BeatmapSetsByID.Add(set.BeatmapSet.ID, new List<CarouselBeatmapSet> { set });
|
||||||
|
|
||||||
base.AddItem(i);
|
base.AddItem(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CarouselBeatmapSet? RemoveChild(Guid beatmapSetID)
|
public IEnumerable<CarouselBeatmapSet> RemoveItemsByID(Guid beatmapSetID)
|
||||||
{
|
{
|
||||||
if (BeatmapSetsByID.TryGetValue(beatmapSetID, out var carouselBeatmapSet))
|
if (BeatmapSetsByID.TryGetValue(beatmapSetID, out var carouselBeatmapSets))
|
||||||
{
|
{
|
||||||
RemoveItem(carouselBeatmapSet);
|
foreach (var set in carouselBeatmapSets)
|
||||||
return carouselBeatmapSet;
|
RemoveItem(set);
|
||||||
|
|
||||||
|
return carouselBeatmapSets;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return Enumerable.Empty<CarouselBeatmapSet>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RemoveItem(CarouselItem i)
|
public override void RemoveItem(CarouselItem i)
|
||||||
|
@ -141,9 +141,9 @@ namespace osu.Game.Screens.Select
|
|||||||
LayoutEasing = Easing.OutQuad,
|
LayoutEasing = Easing.OutQuad,
|
||||||
Children = new[]
|
Children = new[]
|
||||||
{
|
{
|
||||||
description = new MetadataSectionDescription(searchOnSongSelect),
|
description = new MetadataSectionDescription(query => songSelect?.Search(query)),
|
||||||
source = new MetadataSectionSource(searchOnSongSelect),
|
source = new MetadataSectionSource(query => songSelect?.Search(query)),
|
||||||
tags = new MetadataSectionTags(searchOnSongSelect),
|
tags = new MetadataSectionTags(query => songSelect?.Search(query)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -176,12 +176,6 @@ namespace osu.Game.Screens.Select
|
|||||||
},
|
},
|
||||||
loading = new LoadingLayer(true)
|
loading = new LoadingLayer(true)
|
||||||
};
|
};
|
||||||
|
|
||||||
void searchOnSongSelect(string text)
|
|
||||||
{
|
|
||||||
if (songSelect != null)
|
|
||||||
songSelect.FilterControl.CurrentTextSearch.Value = text;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateStatistics()
|
private void updateStatistics()
|
||||||
|
@ -30,6 +30,7 @@ using osu.Game.Rulesets;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Resources.Localisation.Web;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Select
|
namespace osu.Game.Screens.Select
|
||||||
{
|
{
|
||||||
@ -371,7 +372,7 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
new InfoLabel(new BeatmapStatistic
|
new InfoLabel(new BeatmapStatistic
|
||||||
{
|
{
|
||||||
Name = $"Length (Drain: {playableBeatmap.CalculateDrainLength().ToFormattedDuration().ToString()})",
|
Name = BeatmapsetsStrings.ShowStatsTotalLength(playableBeatmap.CalculateDrainLength().ToFormattedDuration()),
|
||||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
|
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
|
||||||
Content = working.BeatmapInfo.Length.ToFormattedDuration().ToString(),
|
Content = working.BeatmapInfo.Length.ToFormattedDuration().ToString(),
|
||||||
}),
|
}),
|
||||||
@ -415,7 +416,7 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic
|
bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic
|
||||||
{
|
{
|
||||||
Name = "BPM",
|
Name = BeatmapsetsStrings.ShowStatsBpm,
|
||||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm),
|
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm),
|
||||||
Content = labelText
|
Content = labelText
|
||||||
});
|
});
|
||||||
|
329
osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs
Normal file
329
osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
// 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.Threading;
|
||||||
|
using osuTK;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Effects;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.Drawables;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Select
|
||||||
|
{
|
||||||
|
public partial class BeatmapInfoWedgeV2 : VisibilityContainer
|
||||||
|
{
|
||||||
|
public const float WEDGE_HEIGHT = 120;
|
||||||
|
private const float shear_width = 21;
|
||||||
|
private const float transition_duration = 250;
|
||||||
|
private const float corner_radius = 10;
|
||||||
|
private const float colour_bar_width = 30;
|
||||||
|
|
||||||
|
/// Todo: move this const out to song select when more new design elements are implemented for the beatmap details area, since it applies to text alignment of various elements
|
||||||
|
private const float text_margin = 62;
|
||||||
|
|
||||||
|
private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / WEDGE_HEIGHT, 0);
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuColour colours { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
|
||||||
|
|
||||||
|
protected Container? DisplayedContent { get; private set; }
|
||||||
|
|
||||||
|
protected WedgeInfoText? Info { get; private set; }
|
||||||
|
|
||||||
|
private Container difficultyColourBar = null!;
|
||||||
|
private StarCounter starCounter = null!;
|
||||||
|
private StarRatingDisplay starRatingDisplay = null!;
|
||||||
|
private BeatmapSetOnlineStatusPill statusPill = null!;
|
||||||
|
private Container content = null!;
|
||||||
|
|
||||||
|
private IBindable<StarDifficulty?>? starDifficulty;
|
||||||
|
private CancellationTokenSource? cancellationSource;
|
||||||
|
|
||||||
|
public BeatmapInfoWedgeV2()
|
||||||
|
{
|
||||||
|
Height = WEDGE_HEIGHT;
|
||||||
|
Shear = wedged_container_shear;
|
||||||
|
Masking = true;
|
||||||
|
Margin = new MarginPadding { Left = -corner_radius };
|
||||||
|
EdgeEffect = new EdgeEffectParameters
|
||||||
|
{
|
||||||
|
Colour = Colour4.Black.Opacity(0.2f),
|
||||||
|
Type = EdgeEffectType.Shadow,
|
||||||
|
Radius = 3,
|
||||||
|
};
|
||||||
|
CornerRadius = corner_radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Child = content = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
// These elements can't be grouped with the rest of the content, due to being present either outside or under the backgrounds area
|
||||||
|
difficultyColourBar = new Container
|
||||||
|
{
|
||||||
|
Colour = Colour4.Transparent,
|
||||||
|
Depth = float.MaxValue,
|
||||||
|
Anchor = Anchor.TopRight,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
|
||||||
|
// By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it.
|
||||||
|
Width = colour_bar_width + corner_radius,
|
||||||
|
Child = new Box { RelativeSizeAxes = Axes.Both }
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
// Applying the shear to this container and nesting the starCounter inside avoids
|
||||||
|
// the deformation that occurs if the shear is applied to the starCounter whilst rotated
|
||||||
|
Shear = -wedged_container_shear,
|
||||||
|
X = -colour_bar_width / 2,
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
Width = colour_bar_width,
|
||||||
|
Child = starCounter = new StarCounter
|
||||||
|
{
|
||||||
|
Rotation = (float)(Math.Atan(shear_width / WEDGE_HEIGHT) * (180 / Math.PI)),
|
||||||
|
Colour = Colour4.Transparent,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Scale = new Vector2(0.35f),
|
||||||
|
Direction = FillDirection.Vertical
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
Name = "Topright-aligned metadata",
|
||||||
|
Anchor = Anchor.TopRight,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Padding = new MarginPadding { Top = 3, Right = colour_bar_width + 8 },
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Spacing = new Vector2(0, 5),
|
||||||
|
Depth = float.MinValue,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
starRatingDisplay = new StarRatingDisplay(default, animated: true)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopRight,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
|
Shear = -wedged_container_shear,
|
||||||
|
Alpha = 0,
|
||||||
|
},
|
||||||
|
statusPill = new BeatmapSetOnlineStatusPill
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.TopRight,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
|
Shear = -wedged_container_shear,
|
||||||
|
TextSize = 11,
|
||||||
|
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
|
||||||
|
Alpha = 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
ruleset.BindValueChanged(_ => updateDisplay());
|
||||||
|
|
||||||
|
starRatingDisplay.Current.BindValueChanged(s =>
|
||||||
|
{
|
||||||
|
// use actual stars as star counter has its own animation
|
||||||
|
starCounter.Current = (float)s.NewValue.Stars;
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
starRatingDisplay.DisplayedStars.BindValueChanged(s =>
|
||||||
|
{
|
||||||
|
// sync color with star rating display
|
||||||
|
starCounter.Colour = s.NewValue >= 6.5 ? colours.Orange1 : Colour4.Black.Opacity(0.75f);
|
||||||
|
difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue));
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private const double animation_duration = 600;
|
||||||
|
|
||||||
|
protected override void PopIn()
|
||||||
|
{
|
||||||
|
this.MoveToX(0, animation_duration, Easing.OutQuint);
|
||||||
|
this.FadeIn(200, Easing.In);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopOut()
|
||||||
|
{
|
||||||
|
this.MoveToX(-150, animation_duration, Easing.OutQuint);
|
||||||
|
this.FadeOut(200, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorkingBeatmap beatmap = null!;
|
||||||
|
|
||||||
|
public WorkingBeatmap Beatmap
|
||||||
|
{
|
||||||
|
get => beatmap;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (beatmap == value) return;
|
||||||
|
|
||||||
|
beatmap = value;
|
||||||
|
|
||||||
|
updateDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Container? loadingInfo;
|
||||||
|
|
||||||
|
private void updateDisplay()
|
||||||
|
{
|
||||||
|
statusPill.Status = beatmap.BeatmapInfo.Status;
|
||||||
|
|
||||||
|
starDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token);
|
||||||
|
|
||||||
|
starDifficulty.BindValueChanged(s =>
|
||||||
|
{
|
||||||
|
starRatingDisplay.Current.Value = s.NewValue ?? default;
|
||||||
|
|
||||||
|
starRatingDisplay.FadeIn(transition_duration);
|
||||||
|
});
|
||||||
|
|
||||||
|
Scheduler.AddOnce(() =>
|
||||||
|
{
|
||||||
|
LoadComponentAsync(loadingInfo = new Container
|
||||||
|
{
|
||||||
|
Padding = new MarginPadding { Right = colour_bar_width },
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Depth = DisplayedContent?.Depth + 1 ?? 0,
|
||||||
|
Child = new Container
|
||||||
|
{
|
||||||
|
Masking = true,
|
||||||
|
CornerRadius = corner_radius,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
// TODO: New wedge design uses a coloured horizontal gradient for its background, however this lacks implementation information in the figma draft.
|
||||||
|
// pending https://www.figma.com/file/DXKwqZhD5yyb1igc3mKo1P?node-id=2980:3361#340801912 being answered.
|
||||||
|
new BeatmapInfoWedgeBackground(beatmap) { Shear = -Shear },
|
||||||
|
Info = new WedgeInfoText(beatmap) { Shear = -Shear }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, d =>
|
||||||
|
{
|
||||||
|
// Ensure we are the most recent loaded wedge.
|
||||||
|
if (d != loadingInfo) return;
|
||||||
|
|
||||||
|
removeOldInfo();
|
||||||
|
content.Add(DisplayedContent = d);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
void removeOldInfo()
|
||||||
|
{
|
||||||
|
DisplayedContent?.FadeOut(transition_duration);
|
||||||
|
DisplayedContent?.Expire();
|
||||||
|
DisplayedContent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
cancellationSource?.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class WedgeInfoText : Container
|
||||||
|
{
|
||||||
|
public OsuSpriteText TitleLabel { get; private set; } = null!;
|
||||||
|
public OsuSpriteText ArtistLabel { get; private set; } = null!;
|
||||||
|
|
||||||
|
private readonly WorkingBeatmap working;
|
||||||
|
|
||||||
|
public WedgeInfoText(WorkingBeatmap working)
|
||||||
|
{
|
||||||
|
this.working = working;
|
||||||
|
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(SongSelect? songSelect, LocalisationManager localisation)
|
||||||
|
{
|
||||||
|
var metadata = working.Metadata;
|
||||||
|
|
||||||
|
var titleText = new RomanisableString(metadata.TitleUnicode, metadata.Title);
|
||||||
|
var artistText = new RomanisableString(metadata.ArtistUnicode, metadata.Artist);
|
||||||
|
|
||||||
|
Child = new FillFlowContainer
|
||||||
|
{
|
||||||
|
Name = "Top-left aligned metadata",
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Padding = new MarginPadding { Left = text_margin, Top = 12 },
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new OsuHoverContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Action = () => songSelect?.Search(titleText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)),
|
||||||
|
Child = TitleLabel = new TruncatingSpriteText
|
||||||
|
{
|
||||||
|
Shadow = true,
|
||||||
|
Text = titleText,
|
||||||
|
Font = OsuFont.TorusAlternate.With(size: 40, weight: FontWeight.SemiBold),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new OsuHoverContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Action = () => songSelect?.Search(artistText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)),
|
||||||
|
Child = ArtistLabel = new TruncatingSpriteText
|
||||||
|
{
|
||||||
|
// TODO : figma design has a diffused shadow, instead of the solid one present here, not possible currently as far as i'm aware.
|
||||||
|
Shadow = true,
|
||||||
|
Text = artistText,
|
||||||
|
// Not sure if this should be semi bold or medium
|
||||||
|
Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void UpdateAfterChildren()
|
||||||
|
{
|
||||||
|
base.UpdateAfterChildren();
|
||||||
|
|
||||||
|
// best effort to confine the auto-sized text to wedge bounds
|
||||||
|
// the artist label doesn't have an extra text_margin as it doesn't touch the right metadata
|
||||||
|
TitleLabel.MaxWidth = DrawWidth - text_margin * 2 - shear_width;
|
||||||
|
ArtistLabel.MaxWidth = DrawWidth - text_margin - shear_width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,11 @@ namespace osu.Game.Screens.Select
|
|||||||
public GroupMode Group;
|
public GroupMode Group;
|
||||||
public SortMode Sort;
|
public SortMode Sort;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the display of beatmap sets should be split apart per-difficulty for the current criteria.
|
||||||
|
/// </summary>
|
||||||
|
public bool SplitOutDifficulties => Sort == SortMode.Difficulty;
|
||||||
|
|
||||||
public BeatmapSetInfo? SelectedBeatmapSet;
|
public BeatmapSetInfo? SelectedBeatmapSet;
|
||||||
|
|
||||||
public OptionalRange<double> StarDifficulty;
|
public OptionalRange<double> StarDifficulty;
|
||||||
|
196
osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs
Normal file
196
osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
// 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.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.LocalisationExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Collections;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Localisation;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
using osuTK.Input;
|
||||||
|
using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Select.FooterV2
|
||||||
|
{
|
||||||
|
public partial class BeatmapOptionsPopover : OsuPopover
|
||||||
|
{
|
||||||
|
private FillFlowContainer buttonFlow = null!;
|
||||||
|
private readonly FooterButtonOptionsV2 footerButton;
|
||||||
|
|
||||||
|
private WorkingBeatmap beatmapWhenOpening = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||||
|
|
||||||
|
public BeatmapOptionsPopover(FooterButtonOptionsV2 footerButton)
|
||||||
|
{
|
||||||
|
this.footerButton = footerButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(ManageCollectionsDialog? manageCollectionsDialog, SongSelect? songSelect, OsuColour colours, BeatmapManager? beatmapManager)
|
||||||
|
{
|
||||||
|
Content.Padding = new MarginPadding(5);
|
||||||
|
|
||||||
|
Child = buttonFlow = new FillFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Spacing = new Vector2(3),
|
||||||
|
};
|
||||||
|
|
||||||
|
beatmapWhenOpening = beatmap.Value;
|
||||||
|
|
||||||
|
addHeader(CommonStrings.General);
|
||||||
|
addButton(SongSelectStrings.ManageCollections, FontAwesome.Solid.Book, () => manageCollectionsDialog?.Show());
|
||||||
|
|
||||||
|
addHeader(SongSelectStrings.ForAllDifficulties, beatmapWhenOpening.BeatmapSetInfo.ToString());
|
||||||
|
addButton(SongSelectStrings.DeleteBeatmap, FontAwesome.Solid.Trash, () => songSelect?.DeleteBeatmap(beatmapWhenOpening.BeatmapSetInfo), colours.Red1);
|
||||||
|
|
||||||
|
addHeader(SongSelectStrings.ForSelectedDifficulty, beatmapWhenOpening.BeatmapInfo.DifficultyName);
|
||||||
|
// TODO: make work, and make show "unplayed" or "played" based on status.
|
||||||
|
addButton(SongSelectStrings.MarkAsPlayed, FontAwesome.Regular.TimesCircle, null);
|
||||||
|
addButton(SongSelectStrings.ClearAllLocalScores, FontAwesome.Solid.Eraser, () => songSelect?.ClearScores(beatmapWhenOpening.BeatmapInfo), colours.Red1);
|
||||||
|
|
||||||
|
if (songSelect != null && songSelect.AllowEditing)
|
||||||
|
addButton(SongSelectStrings.EditBeatmap, FontAwesome.Solid.PencilAlt, () => songSelect.Edit(beatmapWhenOpening.BeatmapInfo));
|
||||||
|
|
||||||
|
addButton(WebCommonStrings.ButtonsHide.ToSentence(), FontAwesome.Solid.Magic, () => beatmapManager?.Hide(beatmapWhenOpening.BeatmapInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(this));
|
||||||
|
|
||||||
|
beatmap.BindValueChanged(_ => Hide());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OverlayColourProvider overlayColourProvider { get; set; } = null!;
|
||||||
|
|
||||||
|
private void addHeader(LocalisableString text, string? context = null)
|
||||||
|
{
|
||||||
|
var textFlow = new OsuTextFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Padding = new MarginPadding(10),
|
||||||
|
};
|
||||||
|
|
||||||
|
textFlow.AddText(text, t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold));
|
||||||
|
|
||||||
|
if (context != null)
|
||||||
|
{
|
||||||
|
textFlow.NewLine();
|
||||||
|
textFlow.AddText(context, t =>
|
||||||
|
{
|
||||||
|
t.Colour = overlayColourProvider.Content2;
|
||||||
|
t.Font = t.Font.With(size: 13);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonFlow.Add(textFlow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addButton(LocalisableString text, IconUsage icon, Action? action, Color4? colour = null)
|
||||||
|
{
|
||||||
|
var button = new OptionButton
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
Icon = icon,
|
||||||
|
TextColour = colour,
|
||||||
|
Action = () =>
|
||||||
|
{
|
||||||
|
Scheduler.AddDelayed(Hide, 50);
|
||||||
|
action?.Invoke();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
buttonFlow.Add(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class OptionButton : OsuButton
|
||||||
|
{
|
||||||
|
public IconUsage Icon { get; init; }
|
||||||
|
public Color4? TextColour { get; init; }
|
||||||
|
|
||||||
|
public OptionButton()
|
||||||
|
{
|
||||||
|
Size = new Vector2(265, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OverlayColourProvider colourProvider)
|
||||||
|
{
|
||||||
|
BackgroundColour = colourProvider.Background3;
|
||||||
|
|
||||||
|
SpriteText.Colour = TextColour ?? Color4.White;
|
||||||
|
Content.CornerRadius = 10;
|
||||||
|
|
||||||
|
Add(new SpriteIcon
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Size = new Vector2(17),
|
||||||
|
X = 15,
|
||||||
|
Icon = Icon,
|
||||||
|
Colour = TextColour ?? Color4.White,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override SpriteText CreateText() => new OsuSpriteText
|
||||||
|
{
|
||||||
|
Depth = -1,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
X = 40
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
|
{
|
||||||
|
// don't absorb control as ToolbarRulesetSelector uses control + number to navigate
|
||||||
|
if (e.ControlPressed) return false;
|
||||||
|
|
||||||
|
if (!e.Repeat && e.Key >= Key.Number1 && e.Key <= Key.Number9)
|
||||||
|
{
|
||||||
|
int requested = e.Key - Key.Number1;
|
||||||
|
|
||||||
|
OptionButton? found = buttonFlow.Children.OfType<OptionButton>().ElementAtOrDefault(requested);
|
||||||
|
|
||||||
|
if (found != null)
|
||||||
|
{
|
||||||
|
found.TriggerClick();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnKeyDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void UpdateState(ValueChangedEvent<Visibility> state)
|
||||||
|
{
|
||||||
|
base.UpdateState(state);
|
||||||
|
|
||||||
|
if (state.NewValue == Visibility.Hidden)
|
||||||
|
footerButton.IsActive.Value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,14 +2,21 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Select.FooterV2
|
namespace osu.Game.Screens.Select.FooterV2
|
||||||
{
|
{
|
||||||
public partial class FooterButtonOptionsV2 : FooterButtonV2
|
public partial class FooterButtonOptionsV2 : FooterButtonV2, IHasPopover
|
||||||
{
|
{
|
||||||
|
public readonly BindableBool IsActive = new BindableBool();
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colour)
|
private void load(OsuColour colour)
|
||||||
{
|
{
|
||||||
@ -17,6 +24,34 @@ namespace osu.Game.Screens.Select.FooterV2
|
|||||||
Icon = FontAwesome.Solid.Cog;
|
Icon = FontAwesome.Solid.Cog;
|
||||||
AccentColour = colour.Purple1;
|
AccentColour = colour.Purple1;
|
||||||
Hotkey = GlobalAction.ToggleBeatmapOptions;
|
Hotkey = GlobalAction.ToggleBeatmapOptions;
|
||||||
|
|
||||||
|
Action = () => IsActive.Toggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
IsActive.BindValueChanged(active =>
|
||||||
|
{
|
||||||
|
OverlayState.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden;
|
||||||
|
});
|
||||||
|
|
||||||
|
OverlayState.BindValueChanged(state =>
|
||||||
|
{
|
||||||
|
switch (state.NewValue)
|
||||||
|
{
|
||||||
|
case Visibility.Hidden:
|
||||||
|
this.HidePopover();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Visibility.Visible:
|
||||||
|
this.ShowPopover();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Popover GetPopover() => new BeatmapOptionsPopover(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,11 +48,17 @@ namespace osu.Game.Screens.Select.FooterV2
|
|||||||
|
|
||||||
private FillFlowContainer<FooterButtonV2> buttons = null!;
|
private FillFlowContainer<FooterButtonV2> buttons = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
public FooterV2()
|
||||||
private void load(OverlayColourProvider colourProvider)
|
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
Height = height;
|
Height = height;
|
||||||
|
Anchor = Anchor.BottomLeft;
|
||||||
|
Origin = Anchor.BottomLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OverlayColourProvider colourProvider)
|
||||||
|
{
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
new Box
|
new Box
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user