mirror of
https://github.com/ppy/osu.git
synced 2024-11-13 16:47:46 +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>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.823.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.904.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- 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("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
||||
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]
|
||||
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
addPlacementSteps(times, positions);
|
||||
addPathCheckStep(times, positions);
|
||||
|
||||
AddAssert("slider velocity changed", () => !lastObject.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("slider velocity changed", () => !lastObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -108,11 +108,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
double[] times = { 100, 300 };
|
||||
float[] positions = { 200, 300 };
|
||||
addBlueprintStep(times, positions);
|
||||
AddAssert("default slider velocity", () => hitObject.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("default slider velocity", () => hitObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||
|
||||
addDragStartStep(times[1], positions[1]);
|
||||
AddMouseMoveStep(times[1], 400);
|
||||
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
|
||||
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
||||
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1
|
||||
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
|
||||
}.Yield();
|
||||
|
||||
case IHasDuration endTime:
|
||||
|
@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||
{
|
||||
// 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 requiredVelocity = path.ComputeRequiredVelocity();
|
||||
|
||||
|
@ -28,17 +28,17 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
public int RepeatCount { get; set; }
|
||||
|
||||
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
|
||||
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
|
||||
{
|
||||
Precision = 0.01,
|
||||
MinValue = 0.1,
|
||||
MaxValue = 10
|
||||
};
|
||||
|
||||
public double SliderVelocity
|
||||
public double SliderVelocityMultiplier
|
||||
{
|
||||
get => SliderVelocityBindable.Value;
|
||||
set => SliderVelocityBindable.Value = value;
|
||||
get => SliderVelocityMultiplierBindable.Value;
|
||||
set => SliderVelocityMultiplierBindable.Value = value;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
@ -48,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
private double tickDistanceFactor;
|
||||
|
||||
[JsonIgnore]
|
||||
public double Velocity => velocityFactor * SliderVelocity;
|
||||
public double Velocity => velocityFactor * SliderVelocityMultiplier;
|
||||
|
||||
[JsonIgnore]
|
||||
public double TickDistance => tickDistanceFactor * SliderVelocity;
|
||||
public double TickDistance => tickDistanceFactor * SliderVelocityMultiplier;
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
beatLength = timingPoint.BeatLength * hitObject.LegacyBpmMultiplier.Value;
|
||||
else if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
||||
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
|
||||
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocityMultiplier;
|
||||
else
|
||||
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.
|
||||
// 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.Scoring;
|
||||
|
||||
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.
|
||||
// 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.Scoring;
|
||||
|
||||
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.
|
||||
// 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.Scoring;
|
||||
|
||||
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.
|
||||
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Scoring
|
||||
{
|
||||
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)
|
||||
{
|
||||
switch (result)
|
||||
@ -22,5 +35,12 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
|
||||
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;
|
||||
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))
|
||||
}),
|
||||
RepeatCount = 1,
|
||||
SliderVelocity = 10
|
||||
SliderVelocityMultiplier = 10
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
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
|
||||
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.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
private float? alphaAtMiss;
|
||||
|
||||
[Test]
|
||||
public void TestHitCircleClassicMod()
|
||||
public void TestHitCircleClassicModMiss()
|
||||
{
|
||||
AddStep("Create hit circle", () =>
|
||||
{
|
||||
@ -61,8 +62,27 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No early fade is expected to be applied if the hit circle has been hit.
|
||||
/// </summary>
|
||||
[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", () =>
|
||||
{
|
||||
@ -74,6 +94,16 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddAssert("Opaque when missed", () => alphaAtMiss == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitCircleNoModHit()
|
||||
{
|
||||
AddStep("Create hit circle", () =>
|
||||
{
|
||||
SelectedMods.Value = Array.Empty<Mod>();
|
||||
createCircle(true);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderClassicMod()
|
||||
{
|
||||
@ -100,27 +130,33 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddAssert("Head circle opaque when missed", () => alphaAtMiss == 1);
|
||||
}
|
||||
|
||||
private void createCircle()
|
||||
private TestDrawableHitCircle createCircle(bool shouldHit = false)
|
||||
{
|
||||
alphaAtMiss = null;
|
||||
|
||||
DrawableHitCircle drawableHitCircle = new DrawableHitCircle(new HitCircle
|
||||
TestDrawableHitCircle drawableHitCircle = new TestDrawableHitCircle(new HitCircle
|
||||
{
|
||||
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>())
|
||||
mod.ApplyToDrawableHitObject(drawableHitCircle);
|
||||
|
||||
drawableHitCircle.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
drawableHitCircle.OnNewResult += (_, _) =>
|
||||
drawableHitCircle.OnNewResult += (_, result) =>
|
||||
{
|
||||
alphaAtMiss = drawableHitCircle.Alpha;
|
||||
if (!result.IsHit)
|
||||
alphaAtMiss = drawableHitCircle.Alpha;
|
||||
};
|
||||
|
||||
Child = drawableHitCircle;
|
||||
|
||||
return drawableHitCircle;
|
||||
}
|
||||
|
||||
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.OnLoadComplete += _ =>
|
||||
@ -145,12 +183,36 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||
mod.ApplyToDrawableHitObject(drawableSlider.HeadCircle);
|
||||
|
||||
drawableSlider.HeadCircle.OnNewResult += (_, _) =>
|
||||
drawableSlider.HeadCircle.OnNewResult += (_, result) =>
|
||||
{
|
||||
alphaAtMiss = drawableSlider.HeadCircle.Alpha;
|
||||
if (!result.IsHit)
|
||||
alphaAtMiss = drawableSlider.HeadCircle.Alpha;
|
||||
};
|
||||
};
|
||||
|
||||
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.TypeExtensions;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
@ -32,7 +36,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public partial class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene
|
||||
public partial class TestSceneLegacyHitPolicy : RateAdjustedBeatmapTestScene
|
||||
{
|
||||
private readonly OsuHitWindows referenceHitWindows;
|
||||
|
||||
@ -43,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
/// </summary>
|
||||
private readonly string? exportLocation = null;
|
||||
|
||||
public TestSceneObjectOrderedHitPolicy()
|
||||
public TestSceneLegacyHitPolicy()
|
||||
{
|
||||
referenceHitWindows = new OsuHitWindows();
|
||||
referenceHitWindows.SetDifficulty(0);
|
||||
@ -83,6 +87,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
addJudgementAssert(hitObjects[1], 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -119,6 +124,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
addJudgementAssert(hitObjects[1], 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -155,6 +161,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
addJudgementAssert(hitObjects[1], 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -191,7 +198,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
addJudgementAssert(hitObjects[0], HitResult.Meh);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
||||
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>
|
||||
@ -229,13 +238,15 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
addJudgementAssert(hitObjects[1], HitResult.Ok);
|
||||
addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
|
||||
addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time
|
||||
addClickActionAssert(0, ClickAction.Hit);
|
||||
addClickActionAssert(1, ClickAction.Hit);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
[Test]
|
||||
public void TestMissSliderHeadAndHitAllSliderTicks()
|
||||
public void TestHitCircleBeforeSliderHead()
|
||||
{
|
||||
const double time_slider = 1500;
|
||||
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 } }
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
||||
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
||||
addClickActionAssert(0, ClickAction.Hit);
|
||||
addClickActionAssert(1, ClickAction.Hit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -314,6 +327,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
||||
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
||||
addClickActionAssert(0, ClickAction.Hit);
|
||||
addClickActionAssert(1, ClickAction.Hit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -353,6 +368,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
||||
addClickActionAssert(0, ClickAction.Hit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -391,6 +407,199 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
addJudgementAssert(hitObjects[0], 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)
|
||||
@ -408,17 +617,36 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
private void addJudgementOffsetAssert(OsuHitObject hitObject, double 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 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!;
|
||||
Score score = null!;
|
||||
|
||||
AddStep("set up mods", () =>
|
||||
{
|
||||
mods = new List<Mod> { new OsuModClassic() };
|
||||
|
||||
if (extraMods != null)
|
||||
mods.AddRange(extraMods);
|
||||
});
|
||||
|
||||
AddStep("create beatmap", () =>
|
||||
{
|
||||
var cpi = new ControlPointInfo();
|
||||
@ -461,7 +689,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
ScoreInfo =
|
||||
{
|
||||
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", () =>
|
||||
{
|
||||
SelectedMods.Value = new[] { new OsuModClassic() };
|
||||
SelectedMods.Value = mods.ToArray();
|
||||
|
||||
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("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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
Position = new Vector2(0, 0),
|
||||
SliderVelocity = velocity,
|
||||
SliderVelocityMultiplier = velocity,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
|
@ -349,7 +349,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
StartTime = time_slider_start,
|
||||
Position = new Vector2(0, 0),
|
||||
SliderVelocity = 0.1f,
|
||||
SliderVelocityMultiplier = 0.1f,
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
|
@ -336,6 +336,52 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
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)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
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 List<JudgementResult> judgementResults;
|
||||
|
||||
@ -399,7 +451,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public TestSlider()
|
||||
{
|
||||
SliderVelocity = 0.1f;
|
||||
SliderVelocityMultiplier = 0.1f;
|
||||
|
||||
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.
|
||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
|
||||
GenerateTicks = generateTicksData?.GenerateTicks ?? true,
|
||||
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1,
|
||||
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1,
|
||||
}.Yield();
|
||||
|
||||
case IHasDuration endTimeData:
|
||||
|
@ -85,9 +85,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
BeginPlacement();
|
||||
|
||||
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);
|
||||
|
||||
// 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.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@ -57,7 +58,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
var osuRuleset = (DrawableOsuRuleset)drawableRuleset;
|
||||
|
||||
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;
|
||||
}
|
||||
@ -85,13 +89,16 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
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);
|
||||
double lateMissFadeTime = o.HitObject.HitWindows.WindowFor(HitResult.Meh) - okWindow;
|
||||
o.Delay(okWindow).FadeOut(lateMissFadeTime);
|
||||
if (state != ArmedState.Hit)
|
||||
{
|
||||
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.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
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))]
|
||||
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
|
||||
@ -20,12 +26,13 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
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
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
ExtendedMinValue = -10,
|
||||
ExtendedMaxValue = 11,
|
||||
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
|
||||
};
|
||||
@ -53,5 +60,34 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.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)
|
||||
{
|
||||
// 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.
|
||||
this.TransformTo(nameof(FlashlightDim), e.NewValue ? 0.8f : 0.0f, 50);
|
||||
// If a slider is in a tracking state, a further dim should be applied to the (remaining) visible portion of the playfield.
|
||||
FlashlightDim = e.NewValue ? 0.8f : 0.0f;
|
||||
}
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
|
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
ComboOffset = original.ComboOffset;
|
||||
LegacyLastTickOffset = original.LegacyLastTickOffset;
|
||||
TickDistanceMultiplier = original.TickDistanceMultiplier;
|
||||
SliderVelocity = original.SliderVelocity;
|
||||
SliderVelocityMultiplier = original.SliderVelocityMultiplier;
|
||||
}
|
||||
|
||||
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.Skinning;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
@ -154,12 +155,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if (result == HitResult.None || clickAction != ClickAction.Hit)
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyResult(r =>
|
||||
{
|
||||
|
@ -12,6 +12,8 @@ using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -30,10 +32,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
protected override float SamplePlaybackPosition => CalculateDrawableRelativePosition(this);
|
||||
|
||||
/// <summary>
|
||||
/// Whether this <see cref="DrawableOsuHitObject"/> can be hit, given a time value.
|
||||
/// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false.
|
||||
/// What action this <see cref="DrawableOsuHitObject"/> should take in response to a
|
||||
/// 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>
|
||||
public Func<DrawableHitObject, double, bool> CheckHittable;
|
||||
public Func<DrawableHitObject, double, HitResult, ClickAction> CheckHittable;
|
||||
|
||||
protected DrawableOsuHitObject(OsuHitObject hitObject)
|
||||
: base(hitObject)
|
||||
|
@ -8,6 +8,7 @@ using System.Diagnostics;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
@ -60,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
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()
|
||||
|
@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
public double SpanDuration => Duration / this.SpanCount();
|
||||
|
||||
/// <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>
|
||||
public double Velocity { get; private set; }
|
||||
|
||||
@ -134,17 +134,17 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// </summary>
|
||||
public bool OnlyJudgeNestedObjects = true;
|
||||
|
||||
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
|
||||
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
|
||||
{
|
||||
Precision = 0.01,
|
||||
MinValue = 0.1,
|
||||
MaxValue = 10
|
||||
};
|
||||
|
||||
public double SliderVelocity
|
||||
public double SliderVelocityMultiplier
|
||||
{
|
||||
get => SliderVelocityBindable.Value;
|
||||
set => SliderVelocityBindable.Value = value;
|
||||
get => SliderVelocityMultiplierBindable.Value;
|
||||
set => SliderVelocityMultiplierBindable.Value = value;
|
||||
}
|
||||
|
||||
public bool GenerateTicks { get; set; } = true;
|
||||
@ -167,7 +167,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
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;
|
||||
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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI
|
||||
@ -13,9 +12,9 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
|
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.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI
|
||||
@ -19,8 +20,9 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> 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>
|
||||
bool IsHittable(DrawableHitObject hitObject, double time);
|
||||
ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result);
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable;
|
||||
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.CheckHittable;
|
||||
|
||||
Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}");
|
||||
drawable.OnLoadComplete += onDrawableHitObjectLoaded;
|
||||
|
@ -1,13 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI
|
||||
@ -22,11 +21,14 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
/// </summary>
|
||||
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))
|
||||
{
|
||||
@ -36,22 +38,25 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
// If there is no previous hitobject, allow the hit.
|
||||
if (blockingObject == null)
|
||||
return true;
|
||||
return ClickAction.Hit;
|
||||
|
||||
// A hit is allowed if:
|
||||
// 1. The last blocking hitobject has been judged.
|
||||
// 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).
|
||||
return blockingObject.Judged || time >= blockingObject.HitObject.StartTime;
|
||||
return (blockingObject.Judged || time >= blockingObject.HitObject.StartTime) ? ClickAction.Hit : ClickAction.Shake;
|
||||
}
|
||||
|
||||
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).
|
||||
if (!hitObjectCanBlockFutureHits(hitObject))
|
||||
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!");
|
||||
|
||||
// Miss all hitobjects prior to the hit one.
|
||||
@ -74,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
|
||||
{
|
||||
foreach (var obj in HitObjectContainer.AliveObjects)
|
||||
foreach (var obj in HitObjectContainer!.AliveObjects)
|
||||
{
|
||||
if (obj.HitObject.StartTime >= targetTime)
|
||||
yield break;
|
||||
|
@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
{
|
||||
if (hitObject is not IHasSliderVelocity hasSliderVelocity) continue;
|
||||
|
||||
double nextScrollSpeed = hasSliderVelocity.SliderVelocity;
|
||||
double nextScrollSpeed = hasSliderVelocity.SliderVelocityMultiplier;
|
||||
EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime);
|
||||
|
||||
if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision))
|
||||
@ -189,7 +189,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
if (obj.LegacyBpmMultiplier.HasValue)
|
||||
beatLength = timingPoint.BeatLength * obj.LegacyBpmMultiplier.Value;
|
||||
else if (obj is IHasSliderVelocity hasSliderVelocity)
|
||||
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
|
||||
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocityMultiplier;
|
||||
else
|
||||
beatLength = timingPoint.BeatLength;
|
||||
|
||||
|
@ -78,7 +78,7 @@ namespace osu.Game.Tests.Editing
|
||||
{
|
||||
assertSnapDistance(100, new Slider
|
||||
{
|
||||
SliderVelocity = multiplier
|
||||
SliderVelocityMultiplier = multiplier
|
||||
}, false);
|
||||
}
|
||||
|
||||
@ -88,7 +88,7 @@ namespace osu.Game.Tests.Editing
|
||||
{
|
||||
assertSnapDistance(100 * multiplier, new Slider
|
||||
{
|
||||
SliderVelocity = multiplier
|
||||
SliderVelocityMultiplier = multiplier
|
||||
}, true);
|
||||
}
|
||||
|
||||
@ -112,7 +112,7 @@ namespace osu.Game.Tests.Editing
|
||||
|
||||
var referenceObject = new Slider
|
||||
{
|
||||
SliderVelocity = slider_velocity
|
||||
SliderVelocityMultiplier = slider_velocity
|
||||
};
|
||||
|
||||
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
|
||||
|
@ -47,7 +47,35 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
||||
pill.AutoSizeAxes = Axes.Y;
|
||||
pill.Width = 90;
|
||||
}));
|
||||
|
||||
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))
|
||||
}
|
||||
},
|
||||
SliderVelocity = 2
|
||||
SliderVelocityMultiplier = 2
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("unify slider velocity", () =>
|
||||
{
|
||||
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));
|
||||
@ -194,7 +194,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () =>
|
||||
{
|
||||
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.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@ -16,6 +17,7 @@ using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.GameplayTest;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -203,6 +205,33 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
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 Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;
|
||||
|
@ -134,14 +134,16 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
|
||||
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",
|
||||
},
|
||||
new Badge
|
||||
{
|
||||
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
|
||||
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",
|
||||
|
@ -39,6 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
private BeatmapInfo currentSelection => carousel.SelectedBeatmapInfo;
|
||||
|
||||
private const int set_count = 5;
|
||||
private const int diff_count = 3;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetStore rulesets)
|
||||
@ -111,7 +112,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Test]
|
||||
public void TestScrollPositionMaintainedOnAdd()
|
||||
{
|
||||
loadBeatmaps(count: 1, randomDifficulties: false);
|
||||
loadBeatmaps(setCount: 1);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
@ -124,7 +125,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Test]
|
||||
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));
|
||||
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]
|
||||
public void TestScrollPositionMaintainedOnDelete()
|
||||
{
|
||||
loadBeatmaps(count: 50, randomDifficulties: false);
|
||||
loadBeatmaps(setCount: 50);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
@ -150,7 +151,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Test]
|
||||
public void TestManyPanels()
|
||||
{
|
||||
loadBeatmaps(count: 5000, randomDifficulties: true);
|
||||
loadBeatmaps(setCount: 5000, randomDifficulties: true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -501,6 +502,34 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
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]
|
||||
public void TestSelectionEnteringFromEmptyRuleset()
|
||||
{
|
||||
@ -662,7 +691,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
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.
|
||||
var beatmap = set.Beatmaps.First();
|
||||
@ -709,7 +738,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
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.
|
||||
var beatmap = set.Beatmaps.First();
|
||||
@ -758,32 +787,54 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
|
||||
[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>();
|
||||
|
||||
AddStep("Populuate beatmap sets", () =>
|
||||
{
|
||||
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[2].StarRating = 6 + i;
|
||||
set.Beatmaps[1].StarRating = 6 + i;
|
||||
sets.Add(set);
|
||||
}
|
||||
});
|
||||
|
||||
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));
|
||||
AddAssert("Check first set at end", () => carousel.BeatmapSets.First().Equals(sets.Last()));
|
||||
AddAssert("Check last set at start", () => carousel.BeatmapSets.Last().Equals(sets.First()));
|
||||
checkVisibleItemCount(false, local_set_count);
|
||||
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));
|
||||
AddAssert("Check first set at start", () => carousel.BeatmapSets.First().Equals(sets.First()));
|
||||
AddAssert("Check last set at end", () => carousel.BeatmapSets.Last().Equals(sets.Last()));
|
||||
checkVisibleItemCount(false, local_set_count);
|
||||
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]
|
||||
@ -838,7 +889,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
AddStep("create hidden set", () =>
|
||||
{
|
||||
hidingSet = TestResources.CreateTestBeatmapSetInfo(3);
|
||||
hidingSet = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||
hidingSet.Beatmaps[1].Hidden = true;
|
||||
|
||||
hiddenList.Clear();
|
||||
@ -885,7 +936,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
AddStep("add mixed ruleset beatmapset", () =>
|
||||
{
|
||||
testMixed = TestResources.CreateTestBeatmapSetInfo(3);
|
||||
testMixed = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||
|
||||
for (int i = 0; i <= 2; i++)
|
||||
{
|
||||
@ -907,7 +958,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
BeatmapSetInfo testSingle = null;
|
||||
AddStep("add single ruleset beatmapset", () =>
|
||||
{
|
||||
testSingle = TestResources.CreateTestBeatmapSetInfo(3);
|
||||
testSingle = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||
testSingle.Beatmaps.ForEach(b =>
|
||||
{
|
||||
b.Ruleset = rulesets.AvailableRulesets.ElementAt(1);
|
||||
@ -930,7 +981,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
manySets.Clear();
|
||||
|
||||
for (int i = 1; i <= 50; i++)
|
||||
manySets.Add(TestResources.CreateTestBeatmapSetInfo(3));
|
||||
manySets.Add(TestResources.CreateTestBeatmapSetInfo(diff_count));
|
||||
});
|
||||
|
||||
loadBeatmaps(manySets);
|
||||
@ -955,6 +1006,43 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
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]
|
||||
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,
|
||||
bool randomDifficulties = false)
|
||||
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null,
|
||||
int? setCount = null, int? diffCount = null, bool randomDifficulties = false)
|
||||
{
|
||||
bool changed = false;
|
||||
|
||||
if (beatmapSets == null)
|
||||
{
|
||||
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(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.ControlPoints;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
@ -188,7 +189,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Select.FooterV2;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -37,10 +39,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
footer = new FooterV2
|
||||
new PopoverContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = footer = new FooterV2(),
|
||||
},
|
||||
overlay = new DummyOverlay()
|
||||
};
|
||||
@ -56,6 +58,24 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
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]
|
||||
public void TestState()
|
||||
{
|
||||
|
@ -15,6 +15,7 @@ using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Tests.Online;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
@ -192,6 +193,57 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
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()
|
||||
{
|
||||
return carousel = new BeatmapCarousel
|
||||
@ -199,7 +251,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
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);
|
||||
});
|
||||
|
||||
AddStep("set mods to HR", () => songBar.Mods = LegacyMods.HardRock);
|
||||
AddStep("set mods to DT", () => songBar.Mods = LegacyMods.DoubleTime);
|
||||
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 override void SetUpSteps()
|
||||
{
|
||||
AddStep("clear matches", () => Ladder.Matches.Clear());
|
||||
|
||||
base.SetUpSteps();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@ -34,6 +41,36 @@ namespace osu.Game.Tournament.Tests.Screens
|
||||
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)
|
||||
// Humanizer cannot handle negative timespans.
|
||||
=> AddStep($"start time is {relativeTime}", () =>
|
||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Tournament.Tests
|
||||
{
|
||||
public TournamentScalingContainer()
|
||||
{
|
||||
TargetDrawSize = new Vector2(1920, 1080);
|
||||
TargetDrawSize = new Vector2(1024, 768);
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
|
@ -92,8 +92,16 @@ namespace osu.Game.Tournament.IPC
|
||||
else
|
||||
{
|
||||
beatmapLookupRequest = new GetBeatmapRequest(new APIBeatmap { OnlineID = beatmapId });
|
||||
beatmapLookupRequest.Success += b => Beatmap.Value = new TournamentBeatmap(b);
|
||||
beatmapLookupRequest.Failure += _ => Beatmap.Value = null;
|
||||
beatmapLookupRequest.Success += b =>
|
||||
{
|
||||
if (lastBeatmapId == beatmapId)
|
||||
Beatmap.Value = new TournamentBeatmap(b);
|
||||
};
|
||||
beatmapLookupRequest.Failure += _ =>
|
||||
{
|
||||
if (lastBeatmapId == beatmapId)
|
||||
Beatmap.Value = null;
|
||||
};
|
||||
API.Queue(beatmapLookupRequest);
|
||||
}
|
||||
}
|
||||
|
@ -7,13 +7,16 @@ using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
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.Online.Multiplayer;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tournament
|
||||
{
|
||||
internal partial class SaveChangesOverlay : CompositeDrawable
|
||||
internal partial class SaveChangesOverlay : CompositeDrawable, IKeyBindingHandler<PlatformAction>
|
||||
{
|
||||
[Resolved]
|
||||
private TournamentGame tournamentGame { get; set; } = null!;
|
||||
@ -78,6 +81,21 @@ namespace osu.Game.Tournament
|
||||
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 saveChanges()
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -19,6 +20,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
||||
{
|
||||
public partial class ScheduleScreen : TournamentScreen
|
||||
{
|
||||
private readonly BindableList<TournamentMatch> allMatches = new BindableList<TournamentMatch>();
|
||||
private readonly Bindable<TournamentMatch?> currentMatch = new Bindable<TournamentMatch?>();
|
||||
private Container mainContainer = null!;
|
||||
private LadderInfo ladder = null!;
|
||||
@ -101,19 +103,34 @@ namespace osu.Game.Tournament.Screens.Schedule
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
allMatches.BindTo(ladder.Matches);
|
||||
allMatches.BindCollectionChanged((_, _) => refresh());
|
||||
|
||||
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);
|
||||
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))));
|
||||
const int days_for_displays = 4;
|
||||
|
||||
upcoming = upcoming.Concat(conditionals);
|
||||
upcoming = upcoming.OrderBy(p => p.Date.Value).Take(8);
|
||||
IEnumerable<ConditionalTournamentMatch> conditionals =
|
||||
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;
|
||||
|
||||
@ -137,12 +154,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.4f,
|
||||
ChildrenEnumerable = 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)
|
||||
.OrderByDescending(p => p.Date.Value)
|
||||
.Take(8)
|
||||
.Select(p => new ScheduleMatch(p))
|
||||
ChildrenEnumerable = recent.Select(p => new ScheduleMatch(p))
|
||||
},
|
||||
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
|
||||
{
|
||||
@ -170,12 +182,12 @@ namespace osu.Game.Tournament.Screens.Schedule
|
||||
Spacing = new Vector2(30),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ScheduleMatch(match.NewValue, false)
|
||||
new ScheduleMatch(currentMatch.Value, false)
|
||||
{
|
||||
Anchor = 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,
|
||||
Origin = Anchor.CentreLeft,
|
||||
@ -185,7 +197,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
||||
{
|
||||
Anchor = 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)
|
||||
},
|
||||
new FillFlowContainer
|
||||
@ -196,7 +208,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
||||
Origin = Anchor.CentreLeft,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ScheduleMatchDate(match.NewValue.Date.Value)
|
||||
new ScheduleMatchDate(currentMatch.Value.Date.Value)
|
||||
{
|
||||
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular)
|
||||
}
|
||||
@ -282,6 +294,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
||||
{
|
||||
Direction = FillDirection.Vertical,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(0, -6),
|
||||
Margin = new MarginPadding(10)
|
||||
},
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
// 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.Drawing;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -48,8 +47,6 @@ namespace osu.Game.Tournament
|
||||
{
|
||||
frameworkConfig.BindWith(FrameworkSetting.WindowedSize, windowSize);
|
||||
|
||||
windowSize.MinValue = new Size(TournamentSceneManager.REQUIRED_WIDTH, TournamentSceneManager.STREAM_AREA_HEIGHT);
|
||||
|
||||
windowMode = frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
|
||||
|
||||
Add(loadingSpinner = new LoadingSpinner(true, true)
|
||||
|
@ -19,6 +19,8 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
{
|
||||
public partial class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip
|
||||
{
|
||||
private const double animation_duration = 400;
|
||||
|
||||
private BeatmapOnlineStatus status;
|
||||
|
||||
public BeatmapOnlineStatus Status
|
||||
@ -32,7 +34,12 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
status = value;
|
||||
|
||||
if (IsLoaded)
|
||||
{
|
||||
AutoSizeDuration = (float)animation_duration;
|
||||
AutoSizeEasing = Easing.OutQuint;
|
||||
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,6 +68,8 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
{
|
||||
Masking = true;
|
||||
|
||||
Alpha = 0;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
background = new Box
|
||||
@ -83,21 +92,32 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
updateState();
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
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)
|
||||
statusText.Colour = status == BeatmapOnlineStatus.Graveyard ? colourProvider.Background1 : colourProvider.Background3;
|
||||
statusTextColour = status == BeatmapOnlineStatus.Graveyard ? colourProvider.Background1 : colourProvider.Background3;
|
||||
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
|
||||
|
@ -111,7 +111,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
}
|
||||
|
||||
if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
||||
hasSliderVelocity.SliderVelocity = difficultyControlPoint.SliderVelocity;
|
||||
hasSliderVelocity.SliderVelocityMultiplier = difficultyControlPoint.SliderVelocity;
|
||||
|
||||
hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||
}
|
||||
|
@ -269,7 +269,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
foreach (var hitObject in hitObjects)
|
||||
{
|
||||
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.Game.Online;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
@ -74,7 +75,7 @@ namespace osu.Game.Graphics.Containers
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -30,6 +30,11 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
private const float star_spacing = 4;
|
||||
|
||||
public virtual FillDirection Direction
|
||||
{
|
||||
set => stars.Direction = value;
|
||||
}
|
||||
|
||||
private float current;
|
||||
|
||||
/// <summary>
|
||||
@ -64,7 +69,6 @@ namespace osu.Game.Graphics.UserInterface
|
||||
stars = new FillFlowContainer<Star>
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(star_spacing),
|
||||
ChildrenEnumerable = Enumerable.Range(0, StarCount).Select(_ => CreateStar())
|
||||
}
|
||||
|
@ -169,6 +169,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
@ -14,11 +14,6 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
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>
|
||||
/// "Show log overlay"
|
||||
/// </summary>
|
||||
|
@ -19,11 +19,6 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString BeatmapHeader => new TranslatableString(getKey(@"beatmap_header"), @"Beatmap");
|
||||
|
||||
/// <summary>
|
||||
/// "General"
|
||||
/// </summary>
|
||||
public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General");
|
||||
|
||||
/// <summary>
|
||||
/// "Audio"
|
||||
/// </summary>
|
||||
|
@ -9,11 +9,6 @@ namespace osu.Game.Localisation
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.GeneralSettings";
|
||||
|
||||
/// <summary>
|
||||
/// "General"
|
||||
/// </summary>
|
||||
public static LocalisableString GeneralSectionHeader => new TranslatableString(getKey(@"general_section_header"), @"General");
|
||||
|
||||
/// <summary>
|
||||
/// "Language"
|
||||
/// </summary>
|
||||
|
@ -19,6 +19,41 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
@ -14,11 +14,6 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
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>
|
||||
/// "Rotate cursor when dragging"
|
||||
/// </summary>
|
||||
|
@ -14,6 +14,6 @@ namespace osu.Game.Online.API.Requests
|
||||
|
||||
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";
|
||||
APIClientSecret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT";
|
||||
APIClientID = "5";
|
||||
SpectatorEndpointUrl = $"{APIEndpointUrl}/spectator";
|
||||
MultiplayerEndpointUrl = $"{APIEndpointUrl}/multiplayer";
|
||||
MetadataEndpointUrl = $"{APIEndpointUrl}/metadata";
|
||||
SpectatorEndpointUrl = $@"{APIEndpointUrl}/signalr/spectator";
|
||||
MultiplayerEndpointUrl = $@"{APIEndpointUrl}/signalr/multiplayer";
|
||||
MetadataEndpointUrl = $@"{APIEndpointUrl}/signalr/metadata";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ namespace osu.Game.Overlays.OSD
|
||||
public partial class CopyUrlToast : Toast
|
||||
{
|
||||
public CopyUrlToast()
|
||||
: base(UserInterfaceStrings.GeneralHeader, ToastStrings.UrlCopied, "")
|
||||
: base(CommonStrings.General, ToastStrings.UrlCopied, "")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background5,
|
||||
Colour = colourProvider.Background4,
|
||||
},
|
||||
new Container // artificial shadow
|
||||
{
|
||||
@ -50,7 +50,7 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
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
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background4,
|
||||
Colour = colourProvider.Background3,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
|
||||
{
|
||||
public partial class GeneralSettings : SettingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => DebugSettingsStrings.GeneralHeader;
|
||||
protected override LocalisableString Header => CommonStrings.General;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
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
|
||||
{
|
||||
protected override LocalisableString Header => GameplaySettingsStrings.GeneralHeader;
|
||||
protected override LocalisableString Header => CommonStrings.General;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
[Resolved(CanBeNull = true)]
|
||||
private OsuGame? game { get; set; }
|
||||
|
||||
public override LocalisableString Header => GeneralSettingsStrings.GeneralSectionHeader;
|
||||
public override LocalisableString Header => CommonStrings.General;
|
||||
|
||||
public override Drawable CreateIcon() => new SpriteIcon
|
||||
{
|
||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
|
||||
{
|
||||
public partial class GeneralSettings : SettingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => UserInterfaceStrings.GeneralHeader;
|
||||
protected override LocalisableString Header => CommonStrings.General;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
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.
|
||||
var adjustedRect = selectionRect;
|
||||
|
||||
// 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;
|
||||
bool isRotated = false;
|
||||
|
||||
// for now aspect lock scale adjustments that occur at corners..
|
||||
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.
|
||||
// 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 dragging from the horizontal centre, only a vertical component is available.
|
||||
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.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.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.
|
||||
var scaledDelta = new Vector2(
|
||||
MathF.Max(adjustedRect.Width / selectionRect.Width, 0),
|
||||
MathF.Max(adjustedRect.Height / selectionRect.Height, 0)
|
||||
adjustedRect.Width / selectionRect.Width,
|
||||
adjustedRect.Height / selectionRect.Height
|
||||
);
|
||||
|
||||
foreach (var b in SelectedBlueprints)
|
||||
@ -102,7 +115,12 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
);
|
||||
|
||||
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;
|
||||
@ -291,7 +309,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
if (parent == null)
|
||||
return drawable.Anchor;
|
||||
|
||||
var screenPosition = getScreenPosition();
|
||||
var screenPosition = drawable.ToScreenSpace(drawable.OriginPosition);
|
||||
|
||||
var absolutePosition = parent.ToLocalSpace(screenPosition);
|
||||
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);
|
||||
|
||||
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)
|
||||
|
@ -244,7 +244,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,14 @@ namespace osu.Game.Rulesets.Mods
|
||||
/// </remarks>
|
||||
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>
|
||||
/// 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;
|
||||
}
|
||||
|
||||
public SliderControl(BindableNumber<float> currentNumber)
|
||||
public SliderControl(BindableNumber<float> currentNumber, Func<BindableNumber<float>, RoundedSliderBar<float>> createSlider)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new RoundedSliderBar<float>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Current = currentNumber,
|
||||
KeyboardStep = 0.1f,
|
||||
}
|
||||
createSlider(currentNumber)
|
||||
};
|
||||
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
@ -34,9 +34,18 @@ namespace osu.Game.Rulesets.Mods
|
||||
set => CurrentNumber.Precision = value;
|
||||
}
|
||||
|
||||
private float minValue;
|
||||
|
||||
public float MinValue
|
||||
{
|
||||
set => CurrentNumber.MinValue = value;
|
||||
set
|
||||
{
|
||||
if (value == minValue)
|
||||
return;
|
||||
|
||||
minValue = value;
|
||||
updateExtents();
|
||||
}
|
||||
}
|
||||
|
||||
private float maxValue;
|
||||
@ -49,7 +58,24 @@ namespace osu.Game.Rulesets.Mods
|
||||
return;
|
||||
|
||||
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;
|
||||
|
||||
extendedMaxValue = value;
|
||||
updateMaxValue();
|
||||
updateExtents();
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,7 +104,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public DifficultyBindable(float? defaultValue = null)
|
||||
: base(defaultValue)
|
||||
{
|
||||
ExtendedLimits.BindValueChanged(_ => updateMaxValue());
|
||||
ExtendedLimits.BindValueChanged(_ => updateExtents());
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -21,8 +21,9 @@ namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Id = APIUser.SYSTEM_USER_ID,
|
||||
Id = replayData.User.OnlineID,
|
||||
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 LocalisableString Description => "Score set on earlier osu! versions with the V2 scoring algorithm active.";
|
||||
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 BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1);
|
||||
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1);
|
||||
|
||||
public double SliderVelocity
|
||||
public double SliderVelocityMultiplier
|
||||
{
|
||||
get => SliderVelocityBindable.Value;
|
||||
set => SliderVelocityBindable.Value = value;
|
||||
get => SliderVelocityMultiplierBindable.Value;
|
||||
set => SliderVelocityMultiplierBindable.Value = value;
|
||||
}
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Objects.Types
|
||||
/// <summary>
|
||||
/// The slider velocity multiplier.
|
||||
/// </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)
|
||||
{
|
||||
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)
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -307,6 +308,25 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
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)
|
||||
{
|
||||
var handle = new SelectionBoxScaleHandle
|
||||
|
@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
|
||||
@ -69,6 +70,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
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 SelectionBoxDragHandle activeHandle;
|
||||
|
||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
HitObject = hitObject;
|
||||
|
||||
speedMultiplier = (hitObject as IHasSliderVelocity)?.SliderVelocityBindable.GetBoundCopy();
|
||||
speedMultiplier = (hitObject as IHasSliderVelocity)?.SliderVelocityMultiplierBindable.GetBoundCopy();
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// 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
|
||||
? ((IHasSliderVelocity)relevantObjects.First()).SliderVelocityBindable
|
||||
var selectedPointBindable = relevantObjects.Select(point => ((IHasSliderVelocity)point).SliderVelocityMultiplier).Distinct().Count() == 1
|
||||
? ((IHasSliderVelocity)relevantObjects.First()).SliderVelocityMultiplierBindable
|
||||
: null;
|
||||
|
||||
if (selectedPointBindable != null)
|
||||
@ -127,7 +127,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
foreach (var h in relevantObjects)
|
||||
{
|
||||
((IHasSliderVelocity)h).SliderVelocity = val.NewValue.Value;
|
||||
((IHasSliderVelocity)h).SliderVelocityMultiplier = val.NewValue.Value;
|
||||
beatmap.Update(h);
|
||||
}
|
||||
|
||||
@ -169,7 +169,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
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)");
|
||||
AddValue($"{beatmapVelocity:#,0.00}x");
|
||||
@ -177,6 +177,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
AddHeader("Final velocity");
|
||||
AddValue($"{beatmapVelocity * current.Value:#,0.00}x");
|
||||
|
||||
if (sliderVelocities.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (sliderVelocities.First() != sliderVelocities.Last())
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
|
||||
hasSliderVelocity.SliderVelocity = newVelocity;
|
||||
hasSliderVelocity.SliderVelocityMultiplier = newVelocity;
|
||||
beatmap.Update(hitObject);
|
||||
}
|
||||
else
|
||||
|
@ -114,7 +114,7 @@ namespace osu.Game.Screens.Edit
|
||||
continue;
|
||||
|
||||
if (oldObject is IHasSliderVelocity oldWithVelocity && newObject is IHasSliderVelocity newWithVelocity)
|
||||
oldWithVelocity.SliderVelocity = newWithVelocity.SliderVelocity;
|
||||
oldWithVelocity.SliderVelocityMultiplier = newWithVelocity.SliderVelocityMultiplier;
|
||||
|
||||
oldObject.Samples = newObject.Samples;
|
||||
|
||||
|
@ -156,7 +156,7 @@ namespace osu.Game.Screens.Ranking
|
||||
if (Score != null)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
|
@ -78,6 +78,8 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private CarouselBeatmapSet? selectedBeatmapSet;
|
||||
|
||||
private List<BeatmapSetInfo> originalBeatmapSetsDetached = new List<BeatmapSetInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the <see cref="SelectedBeatmapInfo"/> is changed.
|
||||
/// </summary>
|
||||
@ -127,15 +129,38 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
|
||||
if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet))
|
||||
selectedBeatmapSet = null;
|
||||
|
||||
Scroll.Clear(false);
|
||||
itemsCache.Invalidate();
|
||||
ScrollToSelected();
|
||||
@ -144,6 +169,15 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
if (loadedTestBeatmaps)
|
||||
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>();
|
||||
@ -330,8 +364,8 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
// Only require to action here if the beatmap is missing.
|
||||
// This avoids processing these events unnecessarily when new beatmaps are imported, for example.
|
||||
if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSet)
|
||||
&& existingSet.BeatmapSet.Beatmaps.All(b => b.ID != beatmapInfo.ID))
|
||||
if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSets)
|
||||
&& existingSets.SelectMany(s => s.Beatmaps).All(b => b.BeatmapInfo.ID != beatmapInfo.ID))
|
||||
{
|
||||
UpdateBeatmapSet(beatmapSet.Detach());
|
||||
}
|
||||
@ -345,15 +379,20 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private void removeBeatmapSet(Guid beatmapSetID) => Schedule(() =>
|
||||
{
|
||||
if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSet))
|
||||
if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSets))
|
||||
return;
|
||||
|
||||
foreach (var beatmap in existingSet.Beatmaps)
|
||||
randomSelectedBeatmaps.Remove(beatmap);
|
||||
originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSetID);
|
||||
|
||||
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();
|
||||
|
||||
if (!Scroll.UserScrolling)
|
||||
@ -366,26 +405,64 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
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 (selectedBeatmapSet?.BeatmapSet.ID == beatmapSet.ID)
|
||||
previouslySelectedID = selectedBeatmap?.BeatmapInfo.ID;
|
||||
|
||||
var newSet = createCarouselSet(beatmapSet);
|
||||
var removedSet = root.RemoveChild(beatmapSet.ID);
|
||||
var removedSets = root.RemoveItemsByID(beatmapSet.ID);
|
||||
|
||||
// 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 (newSet != null)
|
||||
foreach (var removedSet in removedSets)
|
||||
{
|
||||
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.
|
||||
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();
|
||||
@ -632,6 +709,8 @@ namespace osu.Game.Screens.Select
|
||||
applyActiveCriteria(debounce);
|
||||
}
|
||||
|
||||
private bool beatmapsSplitOut;
|
||||
|
||||
private void applyActiveCriteria(bool debounce, bool alwaysResetScrollPosition = true)
|
||||
{
|
||||
PendingFilter?.Cancel();
|
||||
@ -652,6 +731,13 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
PendingFilter = null;
|
||||
|
||||
if (activeCriteria.SplitOutDifficulties != beatmapsSplitOut)
|
||||
{
|
||||
beatmapsSplitOut = activeCriteria.SplitOutDifficulties;
|
||||
loadBeatmapSets(originalBeatmapSetsDetached);
|
||||
return;
|
||||
}
|
||||
|
||||
root.Filter(activeCriteria);
|
||||
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).
|
||||
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)
|
||||
{
|
||||
@ -1069,20 +1155,25 @@ namespace osu.Game.Screens.Select
|
||||
public override void AddItem(CarouselItem 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);
|
||||
}
|
||||
|
||||
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);
|
||||
return carouselBeatmapSet;
|
||||
foreach (var set in carouselBeatmapSets)
|
||||
RemoveItem(set);
|
||||
|
||||
return carouselBeatmapSets;
|
||||
}
|
||||
|
||||
return null;
|
||||
return Enumerable.Empty<CarouselBeatmapSet>();
|
||||
}
|
||||
|
||||
public override void RemoveItem(CarouselItem i)
|
||||
|
@ -141,9 +141,9 @@ namespace osu.Game.Screens.Select
|
||||
LayoutEasing = Easing.OutQuad,
|
||||
Children = new[]
|
||||
{
|
||||
description = new MetadataSectionDescription(searchOnSongSelect),
|
||||
source = new MetadataSectionSource(searchOnSongSelect),
|
||||
tags = new MetadataSectionTags(searchOnSongSelect),
|
||||
description = new MetadataSectionDescription(query => songSelect?.Search(query)),
|
||||
source = new MetadataSectionSource(query => songSelect?.Search(query)),
|
||||
tags = new MetadataSectionTags(query => songSelect?.Search(query)),
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -176,12 +176,6 @@ namespace osu.Game.Screens.Select
|
||||
},
|
||||
loading = new LoadingLayer(true)
|
||||
};
|
||||
|
||||
void searchOnSongSelect(string text)
|
||||
{
|
||||
if (songSelect != null)
|
||||
songSelect.FilterControl.CurrentTextSearch.Value = text;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStatistics()
|
||||
|
@ -30,6 +30,7 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
@ -371,7 +372,7 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
new InfoLabel(new BeatmapStatistic
|
||||
{
|
||||
Name = $"Length (Drain: {playableBeatmap.CalculateDrainLength().ToFormattedDuration().ToString()})",
|
||||
Name = BeatmapsetsStrings.ShowStatsTotalLength(playableBeatmap.CalculateDrainLength().ToFormattedDuration()),
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
|
||||
Content = working.BeatmapInfo.Length.ToFormattedDuration().ToString(),
|
||||
}),
|
||||
@ -415,7 +416,7 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic
|
||||
{
|
||||
Name = "BPM",
|
||||
Name = BeatmapsetsStrings.ShowStatsBpm,
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm),
|
||||
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 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 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.
|
||||
|
||||
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.UserInterface;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Input.Bindings;
|
||||
|
||||
namespace osu.Game.Screens.Select.FooterV2
|
||||
{
|
||||
public partial class FooterButtonOptionsV2 : FooterButtonV2
|
||||
public partial class FooterButtonOptionsV2 : FooterButtonV2, IHasPopover
|
||||
{
|
||||
public readonly BindableBool IsActive = new BindableBool();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colour)
|
||||
{
|
||||
@ -17,6 +24,34 @@ namespace osu.Game.Screens.Select.FooterV2
|
||||
Icon = FontAwesome.Solid.Cog;
|
||||
AccentColour = colour.Purple1;
|
||||
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!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
public FooterV2()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = height;
|
||||
Anchor = Anchor.BottomLeft;
|
||||
Origin = Anchor.BottomLeft;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user